add react-router, auth pages, favicon, notistack

This commit is contained in:
Sidney Alcantara
2022-04-23 01:33:53 +10:00
parent 0e037e4f0c
commit 3f57eabf81
66 changed files with 1914 additions and 245 deletions

View File

@@ -14,7 +14,6 @@
"@mui/icons-material": "^5.6.0",
"@mui/lab": "^5.0.0-alpha.76",
"@mui/material": "^5.6.0",
"@mui/styles": "^5.6.0",
"@rowy/multiselect": "^0.2.3",
"date-fns": "^2.28.0",
"dompurify": "^2.3.6",
@@ -22,6 +21,7 @@
"jotai": "^1.6.4",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"notistack": "^2.0.4",
"react": "^18.0.0",
"react-data-grid": "7.0.0-beta.5",
"react-div-100vh": "^0.7.0",
@@ -31,6 +31,7 @@
"react-helmet-async": "^1.3.0",
"react-router-dom": "^6.3.0",
"react-scripts": "^5.0.0",
"tss-react": "^3.6.2",
"typescript": "^4.6.3",
"web-vitals": "^2.1.4"
},

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 620 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 810 B

3
public/favicon/icon.svg Executable file
View File

@@ -0,0 +1,3 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path d="M13 0a3 3 0 010 6l-2-.001V6H6v7a3 3 0 01-6 0V3a3 3 0 015.501-1.657A2.989 2.989 0 018 0h5zM5 11H1v2a2 2 0 001.85 1.995L3 15a2 2 0 001.995-1.85L5 13v-2zm0-5H1v4h4V6zM3 1a2 2 0 00-1.995 1.85L1 3v2h4V3a2 2 0 00-1.85-1.995L3 1zm8.001 0v4H13a2 2 0 001.995-1.85L15 3a2 2 0 00-1.85-1.995L13 1h-1.999zM10 1H8a2 2 0 00-1.995 1.85L6 3v2h4V1z" fill="#4200FF" fill-rule="nonzero"/>
</svg>

After

Width:  |  Height:  |  Size: 451 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.14, written by Peter Selinger 2001-2017
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1105 6984 c-512 -91 -904 -436 -1050 -925 -23 -76 -28 -99 -45 -209
-11 -72 -11 -4640 0 -4705 5 -27 12 -67 15 -88 17 -99 74 -260 131 -365 188
-346 512 -584 909 -669 118 -25 392 -24 505 2 14 4 38 9 54 11 49 9 197 64
271 101 314 157 564 449 669 781 62 196 59 113 60 1859 1 1054 4 1593 11 1594
6 1 725 2 1600 4 1739 3 1652 0 1847 61 142 44 312 135 424 225 16 13 70 64
119 114 176 176 301 411 352 663 18 90 25 344 11 422 -5 30 -11 66 -12 80 -2
14 -9 45 -15 70 -131 493 -516 858 -1022 967 -79 17 -164 18 -1329 18 -685 0
-1265 -3 -1290 -7 -95 -16 -117 -21 -196 -44 -252 -73 -529 -267 -678 -476
l-40 -55 -45 61 c-183 251 -482 438 -799 503 -105 21 -354 25 -457 7z m444
-455 c267 -72 493 -283 587 -550 46 -131 52 -206 50 -704 l-1 -460 -867 -2
c-606 -1 -870 2 -875 10 -8 13 -7 879 1 962 39 399 389 743 784 771 48 4 88 7
89 8 8 6 170 -18 232 -35z m2811 32 c13 -1 15 -103 14 -863 -1 -475 -2 -868
-3 -874 -2 -17 -1738 -17 -1743 -1 -6 19 -1 948 5 991 19 121 78 269 148 373
45 66 162 180 232 226 125 83 260 130 415 145 51 5 848 8 932 3z m1425 -4
c198 -22 377 -109 520 -253 166 -165 256 -384 256 -619 -1 -389 -272 -744
-642 -840 -107 -28 -147 -30 -634 -30 l-470 0 -1 872 -1 872 51 3 c88 5 868 1
921 -5z m-3604 -2186 c4 0 6 -394 5 -873 l-1 -873 -868 0 c-511 0 -871 4 -874
9 -8 12 -8 1720 -1 1732 5 7 1704 12 1739 5z m7 -2665 c-1 -435 -3 -490 -21
-571 -102 -481 -571 -782 -1050 -675 -285 64 -534 286 -629 560 -47 137 -51
186 -50 680 0 234 1 439 1 456 l1 32 874 0 874 0 0 -482z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@@ -2,14 +2,56 @@
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<meta
name="theme-color"
content="#FAF9FB"
media="(prefers-color-scheme: light)"
/>
<meta
name="theme-color"
content="#0F0F12"
media="(prefers-color-scheme: dark)"
/>
<meta name="color-scheme" content="default" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="default" />
<link
rel="apple-touch-icon"
sizes="180x180"
href="%PUBLIC_URL%/favicon/apple-touch-icon.png"
/>
<link
rel="icon"
type="image/png"
sizes="32x32"
href="%PUBLIC_URL%/favicon/favicon-32x32.png"
/>
<link
rel="icon"
type="image/png"
sizes="16x16"
href="%PUBLIC_URL%/favicon/favicon-16x16.png"
/>
<link
rel="icon"
type="image/svg+xml"
href="%PUBLIC_URL%/favicon/icon.svg"
id="favicon-svg"
/>
<link rel="manifest" href="%PUBLIC_URL%/site.webmanifest" />
<link
rel="mask-icon"
href="%PUBLIC_URL%/favicon/safari-pinned-tab.svg"
color="#4200FF"
/>
<meta name="msapplication-TileColor" content="#4200FF" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
@@ -24,8 +66,40 @@
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
<meta name="color-scheme" content="light dark" />
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
<link
rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400;1,700&display=swap"
/>
<title>Rowy</title>
<meta name="title" content="Rowy GCP as easy as ABC" />
<meta
name="description"
content="Build on the Google Cloud Platform in minutes. Manage Firestore data in a spreadsheet-like UI, write Cloud Functions effortlessly in the browser, and connect to third-party apps. Rowy is open source!"
/>
<meta property="og:type" content="website" />
<meta property="og:url" content="https://rowy.io/" />
<meta property="og:title" content="Rowy GCP as easy as ABC" />
<meta
property="og:description"
content="Build on the Google Cloud Platform in minutes. Manage Firestore data in a spreadsheet-like UI, write Cloud Functions effortlessly in the browser, and connect to third-party apps. Rowy is open source!"
/>
<meta property="og:image" content="%PUBLIC_URL%/static/meta.png" />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content="https://rowy.io/" />
<meta property="twitter:title" content="Rowy GCP as easy as ABC" />
<meta
property="twitter:description"
content="Build on the Google Cloud Platform in minutes. Manage Firestore data in a spreadsheet-like UI, write Cloud Functions effortlessly in the browser, and connect to third-party apps. Rowy is open source!"
/>
<meta property="twitter:image" content="%PUBLIC_URL%/static/meta.png" />
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>

View File

@@ -1,6 +1,6 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"short_name": "Rowy",
"name": "Rowy",
"icons": [
{
"src": "favicon.ico",
@@ -8,12 +8,12 @@
"type": "image/x-icon"
},
{
"src": "logo192.png",
"src": "favicon/android-chrome-192x192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"src": "favicon/android-chrome-512x512.png",
"type": "image/png",
"sizes": "512x512"
}

19
public/site.webmanifest Normal file
View File

@@ -0,0 +1,19 @@
{
"name": "Rowy",
"short_name": "Rowy",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#4200ff",
"background_color": "#4200ff",
"display": "standalone"
}

BIN
public/static/meta.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 268 KiB

View File

@@ -1,38 +0,0 @@
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}

View File

@@ -1,55 +1,65 @@
import { lazy, Suspense } from "react";
import { HelmetProvider } from "react-helmet-async";
import { Routes, Route } from "react-router-dom";
import { useAtom } from "jotai";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "@src/components/ErrorFallback";
import { Provider } from "jotai";
import { globalScope } from "@src/atoms/globalScope";
import Loading from "@src/components/Loading";
import ProjectSourceFirebase from "@src/sources/ProjectSourceFirebase";
import NotFound from "@src/pages/NotFound";
import RequireAuth from "@src/layouts/RequireAuth";
import Nav from "@src/layouts/Nav";
import FirebaseProject from "@src/sources/ProjectSourceFirebase";
import ThemeProvider from "@src/theme/ThemeProvider";
import { globalScope } from "@src/atoms/globalScope";
import { currentUserAtom } from "@src/atoms/auth";
import { routes } from "@src/constants/routes";
const AuthPage = lazy(
() => import("@src/pages/Auth" /* webpackChunkName: "AuthPage" */)
);
import JotaiTestPage from "@src/pages/JotaiTest";
import SignOutPage from "@src/pages/Auth/SignOut";
// prettier-ignore
const AuthPage = lazy(() => import("@src/pages/Auth/index" /* webpackChunkName: "AuthPage" */));
// prettier-ignore
const SignUpPage = lazy(() => import("@src/pages/Auth/SignUp" /* webpackChunkName: "Auth/SignUpPage" */));
// prettier-ignore
const JwtAuthPage = lazy(() => import("@src/pages/Auth/JwtAuth" /* webpackChunkName: "Auth/JwtAuthPage" */));
// prettier-ignore
const ImpersonatorAuthPage = lazy(() => import("@src/pages/Auth/ImpersonatorAuth" /* webpackChunkName: "Auth/ImpersonatorAuthPage" */));
export default function App() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
return (
<ErrorBoundary FallbackComponent={ErrorFallback}>
<HelmetProvider>
<Provider scope={globalScope}>
<ThemeProvider>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<Suspense
fallback={
<Loading
fullScreen
message="FirebaseProject suspended"
timeout={0}
delay={0}
/>
}
>
<FirebaseProject />
</Suspense>
<Suspense
fallback={
<Loading
fullScreen
message="AuthPage suspended"
timeout={0}
delay={0}
/>
}
>
<AuthPage />
</Suspense>
</ErrorBoundary>
</ThemeProvider>
</Provider>
</HelmetProvider>
</ErrorBoundary>
<Suspense fallback={<Loading fullScreen />}>
<ProjectSourceFirebase />
{currentUser === undefined ? (
<Loading fullScreen message="Authenticating" />
) : (
<Routes>
<Route path="*" element={<NotFound />} />
<Route path={routes.auth} element={<AuthPage />} />
<Route path={routes.signUp} element={<SignUpPage />} />
<Route path={routes.signOut} element={<SignOutPage />} />
<Route path={routes.jwtAuth} element={<JwtAuthPage />} />
<Route
path={routes.impersonatorAuth}
element={<ImpersonatorAuthPage />}
/>
<Route
path="/"
element={
<RequireAuth>
<Nav />
</RequireAuth>
}
>
<Route path="dash" element={<div>Dash</div>} />
</Route>
<Route path="/jotaiTest" element={<JotaiTestPage />} />
</Routes>
)}
</Suspense>
);
}

View File

@@ -0,0 +1,74 @@
import { Helmet } from "react-helmet-async";
import { use100vh } from "react-div-100vh";
import { useTheme, alpha } from "@mui/material/styles";
import { Box, BoxProps } from "@mui/material";
import bgPattern from "@src/assets/bg-pattern.svg";
import bgPatternDark from "@src/assets/bg-pattern-dark.svg";
export default function BrandedBackground() {
const theme = useTheme();
return (
<Helmet>
<style type="text/css">
{`
body {
background-size: 100%;
background-image: ${
// prettier-ignore
[
`radial-gradient(circle at 85% 100%, ${theme.palette.background.paper} 20%, ${alpha(theme.palette.background.paper, 0)})`,
`radial-gradient(80% 80% at 15% 100%, ${alpha("#FA0", 0.1)} 25%, ${alpha("#F0A", 0.1)} 50%, ${alpha("#F0A", 0)} 100%)`,
`linear-gradient(to top, ${alpha(theme.palette.background.paper, 1)}, ${alpha(theme.palette.background.paper, 0)})`,
`radial-gradient(60% 180% at 100% 15%, ${alpha("#0FA", 0.3)} 25%, ${alpha("#0AF", 0.2)} 50%, ${alpha("#0AF", 0)} 100%)`,
`linear-gradient(${alpha(theme.palette.primary.main, 0.2)}, ${alpha(theme.palette.primary.main, 0.2)})`,
].join(", ")
};
}
body::before {
content: "";
display: block;
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: -1;
background-image: url('${
theme.palette.mode === "dark" ? bgPatternDark : bgPattern
}');
background-size: ${(480 * 10) / 14}px;
mix-blend-mode: overlay;
}
`}
</style>
</Helmet>
);
}
export function Wrapper(props: BoxProps) {
const fullScreenHeight = use100vh() ?? 0;
return (
<Box
{...props}
sx={{
display: "grid",
placeItems: "center",
alignContent: "center",
gap: (theme) => ({ xs: theme.spacing(2), sm: theme.spacing(3) }),
gridAutoRows: "max-content",
minHeight: fullScreenHeight > 0 ? `${fullScreenHeight}px` : "100vh",
pt: (theme) => `max(env(safe-area-inset-top), ${theme.spacing(1)})`,
pb: (theme) => `max(env(safe-area-inset-bottom), ${theme.spacing(1)})`,
pl: (theme) => `max(env(safe-area-inset-left), ${theme.spacing(1)})`,
pr: (theme) => `max(env(safe-area-inset-right), ${theme.spacing(1)})`,
...props.sx,
}}
/>
);
}

25
src/assets/Favicon.tsx Normal file
View File

@@ -0,0 +1,25 @@
import { useEffect } from "react";
import { useTheme } from "@mui/material";
export default function Favicon() {
const theme = useTheme();
useEffect(() => {
const svg = `<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path
d="M13 0a3 3 0 010 6l-2-.001V6H6v7a3 3 0 01-6 0V3a3 3 0 015.501-1.657A2.989 2.989 0 018 0h5zM5 11H1v2a2 2 0 001.85 1.995L3 15a2 2 0 001.995-1.85L5 13v-2zm0-5H1v4h4V6zM3 1a2 2 0 00-1.995 1.85L1 3v2h4V3a2 2 0 00-1.85-1.995L3 1zm8.001 0v4H13a2 2 0 001.995-1.85L15 3a2 2 0 00-1.85-1.995L13 1h-1.999zM10 1H8a2 2 0 00-1.995 1.85L6 3v2h4V1z"
fill="${theme.palette.primary.main}"
fill-rule="nonzero"
/>
</svg>`;
document.getElementById("favicon-svg")?.setAttribute(
"href",
`data:image/svg+xml;utf8,${encodeURIComponent(svg)
.replace(/\n/g, "")
.replace(/\s{2,}/g, "")}`
);
}, [theme.palette.mode, theme.palette.primary.main]);
return null;
}

33
src/assets/Logo.tsx Normal file
View File

@@ -0,0 +1,33 @@
import { SVGProps } from "react";
import { useTheme } from "@mui/material";
export interface ILogoProps extends SVGProps<SVGSVGElement> {
size?: number;
}
export default function Logo({ size = 1.5, ...props }: ILogoProps) {
const theme = useTheme();
return (
<svg
width={Math.round(68 * size)}
height={Math.round(21 * size)}
viewBox="0 -1.5 68 21"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="rowy-logo-title"
role="img"
{...props}
>
<title id="rowy-logo-title">Rowy</title>
<path
d="M58 3l4 9 4-9h2l-7 16h-2l2-4.5L56 3h2zm-26-.25a6.25 6.25 0 110 12.5 6.25 6.25 0 010-12.5zM26 3v2h-4v10h-2V3h6zm14 0l3 9 3-9h2l3 9 3-9h2l-4 12h-2l-3-9-3 9h-2L38 3h2zm-8 1.75a4.25 4.25 0 100 8.5 4.25 4.25 0 000-8.5z"
fill={theme.palette.text.primary}
/>
<path
d="M13 0a3 3 0 010 6l-2-.001V6H6v7a3 3 0 01-6 0V3a3 3 0 015.501-1.657A2.989 2.989 0 018 0h5zM5 11H1v2a2 2 0 001.85 1.995L3 15a2 2 0 001.995-1.85L5 13v-2zm0-5H1v4h4V6zM3 1a2 2 0 00-1.995 1.85L1 3v2h4V3a2 2 0 00-1.85-1.995L3 1zm8.001 0v4H13a2 2 0 001.995-1.85L15 3a2 2 0 00-1.85-1.995L13 1h-1.999zM10 1H8a2 2 0 00-1.995 1.85L6 3v2h4V1z"
fill={theme.palette.primary.main}
/>
</svg>
);
}

View File

@@ -0,0 +1,44 @@
import { SVGProps } from "react";
import { useTheme } from "@mui/material";
export interface ILogoRowyRunProps extends SVGProps<SVGSVGElement> {
size?: number;
}
export default function LogoRowyRun({
size = 1.5,
...props
}: ILogoRowyRunProps) {
const theme = useTheme();
return (
<svg
width={Math.round(108 * size)}
height={Math.round(26 * size)}
viewBox="0 0 108 26"
xmlns="http://www.w3.org/2000/svg"
aria-labelledby="rowy-run-logo-title"
role="img"
{...props}
>
<title id="rowy-run-logo-title">Rowy Run</title>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M32 7.75a6.25 6.25 0 1 1 0 12.5 6.25 6.25 0 0 1 0-12.5Zm0 2a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM20 20V8h6v2h-4v10h-2Zm24 0 3-9 3 9h2l4-12 5 11.5-2 4.5h2l7-16h-2l-4 9-4-9h-4l-3 9-3-9h-2l-3 9-3-9h-2l4 12h2Z"
fill={theme.palette.text.primary}
/>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 8v10a3 3 0 1 0 6 0v-7h7a3 3 0 1 0 0-6H8a2.997 2.997 0 0 0-2.5 1.341A3 3 0 0 0 0 8Zm10-2H8a2 2 0 0 0-1.995 1.85L6 8v2h4V6Zm-5 4V8a2 2 0 0 0-1.85-1.995L3 6a2 2 0 0 0-1.995 1.85L1 8v2h4Zm0 1H1v4h4v-4Zm-4 5v2a2 2 0 0 0 1.85 1.994L3 20a2 2 0 0 0 1.995-1.85L5 18v-2H1ZM11.001 6H13l.15.005A2 2 0 0 1 15 8l-.005.15A2 2 0 0 1 13 10h-1.999V6Z"
fill={theme.palette.primary.main}
/>
<path
d="M73.25 20h1.825v-8.375c.775-1.475 2.125-2.35 3.2-2.35.425 0 .925.075 1.35.225V7.775c-.275-.1-.725-.175-1.225-.175-1.25 0-2.65.85-3.325 2.175V7.85H73.25V20Zm17.65 0h1.85V7.85h-1.824L90.9 16.3c-.75 1.35-2.25 2.175-3.875 2.175-2.125 0-3.55-1.525-3.55-3.875V7.85H81.65v7c0 3.275 2 5.4 5 5.4 1.775 0 3.45-.825 4.25-2.175V20Zm7.007-12.15h-1.825V20h1.825v-8.475c.75-1.325 2.275-2.15 3.875-2.15 2.125 0 3.55 1.525 3.55 3.875V20h1.825v-7c0-3.275-2-5.4-5-5.4-1.775 0-3.45.825-4.25 2.15v-1.9Z"
fill={theme.palette.primary.main}
/>
</svg>
);
}

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
<svg width="480" height="480" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" stroke="rgba(255, 255, 255, 0.5)" stroke-width="1.5">
<path d="M37 337h6a6 6 0 010 12h-6 0v-12zM296.447 20.204a6 6 0 017.349 4.243l1.552 5.796h0l-11.59 3.105-1.554-5.795a6 6 0 014.243-7.349zM406 51h12v6a6 6 0 01-12 0v-6h0zM394 200h6a6 6 0 010 12h-6 0v-12zM341.447 119.204a6 6 0 017.349 4.243l1.552 5.796h0l-11.59 3.105-1.554-5.795a6 6 0 014.243-7.349zM249.652 206.757l11.59-3.105 1.554 5.795a6 6 0 01-11.592 3.106l-1.552-5.796h0zM322.757 278.652l5.796 1.552a6 6 0 01-3.106 11.592l-5.795-1.553h0l3.105-11.591zM449 295a6 6 0 016 6v6h0-12v-6a6 6 0 016-6zM439 425h3v12h-12v-3a9 9 0 019-9zM333.447 409.204a6 6 0 017.349 4.243l1.552 5.796h0l-11.59 3.105-1.554-5.795a6 6 0 014.243-7.349zM197.804 422.804l10.392 6-6 10.392-10.392-6zM261.45 352.98l2.898.777h0l-3.105 11.591-11.591-3.105.776-2.898a9 9 0 0111.023-6.364zM73.652 440.757l5.795-1.553a6 6 0 013.106 11.592l-5.796 1.552h0l-3.105-11.59zM147.804 332.804l10.392-6 3 5.196a6 6 0 01-10.392 6l-3-5.196h0zM211.757 102.652l11.591 3.105-1.552 5.796a6 6 0 01-11.592-3.106l1.553-5.795h0zM150.652 44.757l5.795-1.553a6 6 0 013.106 11.592l-5.796 1.552h0l-3.105-11.59zM31 200a6 6 0 016 6v6h0-12v-6a6 6 0 016-6zM106.757 131.652l5.796 1.552a6 6 0 01-3.106 11.592l-5.795-1.553h0l3.105-11.591zM91.652 232.757l11.59-3.105 3.106 11.59-11.59 3.106zM57.598 38.304l2.598-1.5h0l6 10.392-10.392 6-1.5-2.598a9 9 0 013.294-12.294z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -0,0 +1,5 @@
<svg width="480" height="480" xmlns="http://www.w3.org/2000/svg">
<g fill="none" fill-rule="evenodd" stroke="rgba(0, 0, 0, 0.75)" stroke-width="1.5">
<path d="M37 337h6a6 6 0 010 12h-6 0v-12zM296.447 20.204a6 6 0 017.349 4.243l1.552 5.796h0l-11.59 3.105-1.554-5.795a6 6 0 014.243-7.349zM406 51h12v6a6 6 0 01-12 0v-6h0zM394 200h6a6 6 0 010 12h-6 0v-12zM341.447 119.204a6 6 0 017.349 4.243l1.552 5.796h0l-11.59 3.105-1.554-5.795a6 6 0 014.243-7.349zM249.652 206.757l11.59-3.105 1.554 5.795a6 6 0 01-11.592 3.106l-1.552-5.796h0zM322.757 278.652l5.796 1.552a6 6 0 01-3.106 11.592l-5.795-1.553h0l3.105-11.591zM449 295a6 6 0 016 6v6h0-12v-6a6 6 0 016-6zM439 425h3v12h-12v-3a9 9 0 019-9zM333.447 409.204a6 6 0 017.349 4.243l1.552 5.796h0l-11.59 3.105-1.554-5.795a6 6 0 014.243-7.349zM197.804 422.804l10.392 6-6 10.392-10.392-6zM261.45 352.98l2.898.777h0l-3.105 11.591-11.591-3.105.776-2.898a9 9 0 0111.023-6.364zM73.652 440.757l5.795-1.553a6 6 0 013.106 11.592l-5.796 1.552h0l-3.105-11.59zM147.804 332.804l10.392-6 3 5.196a6 6 0 01-10.392 6l-3-5.196h0zM211.757 102.652l11.591 3.105-1.552 5.796a6 6 0 01-11.592-3.106l1.553-5.795h0zM150.652 44.757l5.795-1.553a6 6 0 013.106 11.592l-5.796 1.552h0l-3.105-11.59zM31 200a6 6 0 016 6v6h0-12v-6a6 6 0 016-6zM106.757 131.652l5.796 1.552a6 6 0 01-3.106 11.592l-5.795-1.553h0l3.105-11.591zM91.652 232.757l11.59-3.105 3.106 11.59-11.59 3.106zM57.598 38.304l2.598-1.5h0l6 10.392-10.392 6-1.5-2.598a9 9 0 013.294-12.294z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

5
src/assets/favicon.svg Normal file
View File

@@ -0,0 +1,5 @@
<svg width="16" height="16" xmlns="http://www.w3.org/2000/svg">
<path
d="M13 0a3 3 0 010 6l-2-.001V6H6v7a3 3 0 01-6 0V3a3 3 0 015.501-1.657A2.989 2.989 0 018 0h5zM5 11H1v2a2 2 0 001.85 1.995L3 15a2 2 0 001.995-1.85L5 13v-2zm0-5H1v4h4V6zM3 1a2 2 0 00-1.995 1.85L1 3v2h4V3a2 2 0 00-1.85-1.995L3 1zm8.001 0v4H13a2 2 0 001.995-1.85L15 3a2 2 0 00-1.85-1.995L13 1h-1.999zM10 1H8a2 2 0 00-1.995 1.85L6 3v2h4V1z"
fill="currentColor" fill-rule="nonzero" />
</svg>

After

Width:  |  Height:  |  Size: 465 B

View File

@@ -0,0 +1,35 @@
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 72 23">
<g clip-path="url(#a)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M0 15V5A5 5 0 0 1 7.5.669 4.977 4.977 0 0 1 10 0h5a5 5 0 0 1 0 10h-5v5a5 5 0 0 1-10 0Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M20 5a2 2 0 0 1 2-2h6c.721 0 1.354.382 1.705.955A8.212 8.212 0 0 1 34 2.75c1.573 0 3.044.44 4.295 1.205A2 2 0 0 1 40 3h2a2 2 0 0 1 1.897 1.368L45 7.675l1.103-3.307A2 2 0 0 1 48 3h2a2 2 0 0 1 1.897 1.368L53 7.675l1.103-3.307A2 2 0 0 1 56 3h4a2 2 0 0 1 1.828 1.188L64 9.076l2.172-4.888A2 2 0 0 1 68 3h2a2 2 0 0 1 1.832 2.802l-7 16A2 2 0 0 1 63 23h-2a2 2 0 0 1-1.828-2.812l1.643-3.697-2.568-5.907-2.35 7.049A2 2 0 0 1 54 19h-2a2 2 0 0 1-1.897-1.367L49 14.325l-1.103 3.308A2 2 0 0 1 46 19h-2a2 2 0 0 1-1.897-1.367l-.88-2.642A8.253 8.253 0 0 1 26 13.024V17a2 2 0 0 1-2.001 2h-2a2 2 0 0 1-2-2V5Z" fill="#fff"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M34 4.75a6.25 6.25 0 1 1 0 12.5 6.25 6.25 0 0 1 0-12.5Zm0 2a4.25 4.25 0 1 0 0 8.5 4.25 4.25 0 0 0 0-8.5ZM22 17V5h6v2h-4v10h-2Zm24 0 3-9 3 9h2l4-12 5 11.5-2 4.5h2l7-16h-2l-4 9-4-9h-4l-3 9-3-9h-2l-3 9-3-9h-2l4 12h2Z" fill="#000"/>
<g fill-rule="evenodd" clip-rule="evenodd">
<path d="M8 15v-3H2v3a3 3 0 1 0 6 0Zm-5-2h4v2l-.005.15A2 2 0 0 1 5 17l-.15-.006A2 2 0 0 1 3 15v-2Z" fill="url(#b)"/>
<path d="M2 5v3h6V5a3 3 0 0 0-6 0Zm5 2H3V5l.005-.15A2 2 0 0 1 5 3l.15.005A2 2 0 0 1 7 5v2Z" fill="#4200FF"/>
<path d="M8 13V7H2v6h6ZM3 8h4v4H3V8Z" fill="url(#c)"/>
<path d="M15 2h-3v6h3a3 3 0 1 0 0-6Zm-1.999 5V3H15l.15.005A2 2 0 0 1 17 5l-.006.15A2 2 0 0 1 15 7h-1.999Z" fill="url(#d)"/>
<path d="M7 5v3h6V2h-3a3 3 0 0 0-3 3Zm3-2h2v4H8V5l.005-.15A2 2 0 0 1 10 3Z" fill="url(#e)"/>
</g>
</g>
<defs>
<linearGradient id="b" x1="2.5" y1="12.999" x2="2.5" y2="18" gradientUnits="userSpaceOnUse">
<stop stop-color="#F0A"/>
<stop offset="1" stop-color="#FA0"/>
</linearGradient>
<linearGradient id="c" x1="2.488" y1="7.977" x2="2.488" y2="13" gradientUnits="userSpaceOnUse">
<stop stop-color="#4200FF"/>
<stop offset="1" stop-color="#F0A"/>
</linearGradient>
<linearGradient id="d" x1="13.017" y1="7.492" x2="18" y2="7.492" gradientUnits="userSpaceOnUse">
<stop stop-color="#0AF"/>
<stop offset="1" stop-color="#0FA"/>
</linearGradient>
<linearGradient id="e" x1="13" y1="2.498" x2="7.997" y2="2.498" gradientUnits="userSpaceOnUse">
<stop stop-color="#0AF"/>
<stop offset="1" stop-color="#4200FF"/>
</linearGradient>
<clipPath id="a">
<path fill="#fff" d="M0 0h72v23H0z"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="-30 0 315 315" fill="currentColor">
<path d="M213.803 167.03c.442 47.58 41.74 63.413 42.197 63.615-.35 1.116-6.599 22.563-21.757 44.716-13.104 19.153-26.705 38.235-48.13 38.63-21.05.388-27.82-12.483-51.888-12.483-24.061 0-31.582 12.088-51.51 12.871-20.68.783-36.428-20.71-49.64-39.793-27-39.033-47.633-110.3-19.928-158.406 13.763-23.89 38.36-39.017 65.056-39.405 20.307-.387 39.475 13.662 51.889 13.662 12.406 0 35.699-16.895 60.186-14.414 10.25.427 39.026 4.14 57.503 31.186-1.49.923-34.335 20.044-33.978 59.822M174.24 50.199c10.98-13.29 18.369-31.79 16.353-50.199-15.826.636-34.962 10.546-46.314 23.828-10.173 11.763-19.082 30.589-16.678 48.633 17.64 1.365 35.66-8.964 46.64-22.262"/>
</svg>

After

Width:  |  Height:  |  Size: 776 B

View File

@@ -0,0 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M1023.94 511.96c0-282.77-229.23-512-512-512s-512 229.23-512 512c0 255.554 187.231 467.37 432 505.78V659.96h-130v-148h130v-112.8c0-128.32 76.438-199.2 193.39-199.2 56.017 0 114.61 10 114.61 10v126h-64.562c-63.603 0-83.438 39.467-83.438 79.957v96.043h142l-22.7 148h-119.3v357.78c244.769-38.41 432-250.226 432-505.78" fill="#1877F2"/>
<path d="m711.24 659.96 22.7-148h-142v-96.043c0-40.49 19.835-79.957 83.438-79.957h64.562v-126s-58.593-10-114.61-10c-116.952 0-193.39 70.88-193.39 199.2v112.8h-130v148h130v357.78a515.834 515.834 0 0 0 80 6.22c27.216 0 53.933-2.13 80-6.22V659.96h119.3" fill="#FFF"/>
</svg>

After

Width:  |  Height:  |  Size: 686 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="1 1 22 22" width="24" height="24">
<path d="M12 1.27a11 11 0 00-3.48 21.46c.55.09.73-.28.73-.55v-1.84c-3.03.64-3.67-1.46-3.67-1.46-.55-1.29-1.28-1.65-1.28-1.65-.92-.65.1-.65.1-.65 1.1 0 1.73 1.1 1.73 1.1.92 1.65 2.57 1.2 3.21.92a2 2 0 01.64-1.47c-2.47-.27-5.04-1.19-5.04-5.5 0-1.1.46-2.1 1.2-2.84a3.76 3.76 0 010-2.93s.91-.28 3.11 1.1c1.8-.49 3.7-.49 5.5 0 2.1-1.38 3.02-1.1 3.02-1.1a3.76 3.76 0 010 2.93c.83.74 1.2 1.74 1.2 2.94 0 4.21-2.57 5.13-5.04 5.4.45.37.82.92.82 2.02v3.03c0 .27.1.64.73.55A11 11 0 0012 1.27"></path>
</svg>

After

Width:  |  Height:  |  Size: 614 B

View File

@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 19 19">
<path fill="#f25022" d="M0 0h9v9h-9z"/>
<path fill="#00a4ef" d="M0 10h9v9h-9z"/>
<path fill="#7fba00" d="M10 0h9v9h-9z"/>
<path fill="#ffb900" d="M10 10h9v9h-9z"/>
</svg>

After

Width:  |  Height:  |  Size: 240 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMidYMid" viewBox="0 -24 256 256">
<path d="M256 25.45c-9.42 4.177-19.542 7-30.166 8.27 10.845-6.5 19.172-16.793 23.093-29.057a105.183 105.183 0 0 1-33.351 12.745C205.995 7.201 192.346.822 177.239.822c-29.006 0-52.523 23.516-52.523 52.52 0 4.117.465 8.125 1.36 11.97-43.65-2.191-82.35-23.1-108.255-54.876-4.52 7.757-7.11 16.78-7.11 26.404 0 18.222 9.273 34.297 23.365 43.716a52.312 52.312 0 0 1-23.79-6.57c-.003.22-.003.44-.003.661 0 25.447 18.104 46.675 42.13 51.5a52.592 52.592 0 0 1-23.718.9c6.683 20.866 26.08 36.05 49.062 36.475-17.975 14.086-40.622 22.483-65.228 22.483-4.24 0-8.42-.249-12.529-.734 23.243 14.902 50.85 23.597 80.51 23.597 96.607 0 149.434-80.031 149.434-149.435 0-2.278-.05-4.543-.152-6.795A106.748 106.748 0 0 0 256 25.45" fill="#55acee"/>
</svg>

After

Width:  |  Height:  |  Size: 834 B

View File

@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
<g fill="#6001D2" fill-rule="nonzero">
<path d="m10.708 7-2.662 6.42L5.407 7H1l4.912 11.027L4.144 22h4.315L15 7zM15.887 12.877c-1.584 0-2.773 1.191-2.773 2.578 0 1.363 1.143 2.49 2.68 2.49 1.585 0 2.773-1.17 2.773-2.577 0-1.386-1.141-2.49-2.68-2.49M18.987 2.093l-4.381 9.831H19.5l4.38-9.831z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 376 B

View File

@@ -1,5 +1,6 @@
import { atom } from "jotai";
import type { User } from "firebase/auth";
// undefined means loading
export const currentUserAtom = atom<User | null | undefined>(undefined);
export const userRolesAtom = atom<string[]>([]);

View File

@@ -1,5 +1,6 @@
import { atom } from "jotai";
import { DocumentData } from "firebase/firestore";
export const projectIdAtom = atom<string>("");
export const publicSettingsAtom = atom<DocumentData>({});
export const projectSettingsAtom = atom<DocumentData>({});

View File

@@ -0,0 +1,259 @@
import { useAtom } from "jotai";
import StyledFirebaseAuth from "react-firebaseui/StyledFirebaseAuth";
import { Props as FirebaseUiProps } from "react-firebaseui";
import { makeStyles } from "tss-react/mui";
import { Typography } from "@mui/material";
import { alpha } from "@mui/material/styles";
import { globalScope } from "@src/atoms/globalScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
import { publicSettingsAtom } from "@src/atoms/project";
import { defaultUiConfig, getSignInOptions } from "@src/config/firebaseui";
const useStyles = makeStyles()((theme) => ({
root: {
width: "100%",
minHeight: 32,
"& .firebaseui-container": {
backgroundColor: "transparent",
color: theme.palette.text.primary,
fontFamily: theme.typography.fontFamily,
},
"& .firebaseui-text": {
color: theme.palette.text.secondary,
fontFamily: theme.typography.fontFamily,
},
"& .firebaseui-tos": {
...(theme.typography.caption as any),
color: theme.palette.text.disabled,
},
"& .firebaseui-country-selector": {
color: theme.palette.text.primary,
},
"& .firebaseui-title": {
...(theme.typography.h5 as any),
color: theme.palette.text.primary,
},
"& .firebaseui-subtitle": {
...(theme.typography.h6 as any),
color: theme.palette.text.secondary,
},
"& .firebaseui-error": {
...(theme.typography.caption as any),
color: theme.palette.error.main,
},
"& .firebaseui-card-content, & .firebaseui-card-footer": { padding: 0 },
"& .firebaseui-idp-list, & .firebaseui-tenant-list": { margin: 0 },
"& .firebaseui-idp-list>.firebaseui-list-item, & .firebaseui-tenant-list>.firebaseui-list-item":
{
margin: 0,
},
"& .firebaseui-list-item + .firebaseui-list-item": {
paddingTop: theme.spacing(1),
},
"& .mdl-button": {
borderRadius: theme.shape.borderRadius,
...(theme.typography.button as any),
},
"& .mdl-button--raised": {
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[2]}`,
"&:hover": {
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[4]}`,
},
"&:active, &:focus": {
boxShadow: `0 -1px 0 0 rgba(0, 0, 0, 0.12) inset, ${theme.shadows[8]}`,
},
},
"& .mdl-card": {
boxShadow: "none",
minHeight: 0,
},
"& .mdl-button--primary.mdl-button--primary": {
color: theme.palette.primary.main,
},
"& .mdl-button--raised.mdl-button--colored": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastText,
"&:active, &:focus:not(:active), &:hover": {
backgroundColor: theme.palette.primary.main,
},
},
"& .firebaseui-idp-button.mdl-button--raised, & .firebaseui-tenant-button.mdl-button--raised":
{
maxWidth: "none",
minHeight: 32,
padding: theme.spacing(0.5, 1),
backgroundColor: theme.palette.action.input + " !important",
"&:hover": {
backgroundColor: theme.palette.action.hover + " !important",
},
"&:active, &:focus": {
backgroundColor:
theme.palette.action.disabledBackground + " !important",
},
"&, &:hover, &.Mui-disabled": { border: "none" },
"&, &:hover, &:active, &:focus": {
boxShadow: `0 0 0 1px ${theme.palette.action.inputOutline} inset,
0 ${theme.palette.mode === "dark" ? "" : "-"}1px 0 0 ${
theme.palette.action.inputOutline
} inset`,
},
},
"& .firebaseui-idp-icon": {
display: "block",
width: 20,
height: 20,
},
"& .firebaseui-idp-text": {
...(theme.typography.button as any),
color: theme.palette.text.primary,
paddingLeft: theme.spacing(2),
paddingRight: Number(theme.spacing(2).replace("px", "")) + 18,
marginLeft: -18,
width: "100%",
textAlign: "center",
"&.firebaseui-idp-text-long": { display: "none" },
"&.firebaseui-idp-text-short": { display: "table-cell" },
},
"& .firebaseui-idp-google > .firebaseui-idp-text": {
color: theme.palette.text.primary,
},
"& .firebaseui-idp-github .firebaseui-idp-icon, & [data-provider-id='apple.com'] .firebaseui-idp-icon":
{
filter: theme.palette.mode === "dark" ? "invert(1)" : "",
},
"& [data-provider-id='microsoft.com'] .firebaseui-idp-icon": {
width: 21,
height: 21,
position: "relative",
left: -1,
top: -1,
},
"& [data-provider-id='yahoo.com'] > .firebaseui-idp-icon-wrapper > .firebaseui-idp-icon":
{
width: 18,
height: 18,
filter:
theme.palette.mode === "dark"
? "invert(1) saturate(0) brightness(1.5)"
: "",
},
"& .firebaseui-idp-password .firebaseui-idp-icon, & .firebaseui-idp-phone .firebaseui-idp-icon, & .firebaseui-idp-anonymous .firebaseui-idp-icon":
{
width: 24,
height: 24,
position: "relative",
left: -2,
filter: theme.palette.mode === "light" ? "invert(1)" : "",
},
"& .firebaseui-card-header": { padding: 0 },
"& .firebaseui-card-actions": { padding: 0 },
"& .firebaseui-input, & .firebaseui-input-invalid": {
...(theme.typography.body1 as any),
color: theme.palette.text.primary,
},
"& .firebaseui-textfield.mdl-textfield .firebaseui-input": {
borderColor: theme.palette.divider,
},
"& .mdl-textfield.is-invalid .mdl-textfield__input": {
borderColor: theme.palette.error.main,
},
"& .firebaseui-label": {
...(theme.typography.subtitle2 as any),
color: theme.palette.text.secondary,
},
"& .mdl-textfield--floating-label.is-dirty .mdl-textfield__label, .mdl-textfield--floating-label.is-focused .mdl-textfield__label":
{
color: theme.palette.text.primary,
},
"& .firebaseui-textfield.mdl-textfield .firebaseui-label:after": {
backgroundColor: theme.palette.primary.main,
},
"& .mdl-textfield.is-invalid .mdl-textfield__label:after": {
backgroundColor: theme.palette.error.main,
},
"& .mdl-progress>.bufferbar": {
background: alpha(theme.palette.primary.main, 0.33),
},
"& .mdl-progress>.progressbar": {
backgroundColor: theme.palette.primary.main + " !important",
},
},
signInText: {
display: "block",
textAlign: "center",
color: theme.palette.text.secondary,
margin: theme.spacing(-1, 0, -3),
},
skeleton: {
width: "100%",
marginBottom: "calc(var(--spacing-contents) * -1)",
"& > *": {
width: "100%",
height: 32,
borderRadius: theme.shape.borderRadius,
},
"& > * + *": {
marginTop: theme.spacing(1),
},
},
}));
export default function FirebaseUi(props: Partial<FirebaseUiProps>) {
const { classes, cx } = useStyles();
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
const [publicSettings] = useAtom(publicSettingsAtom, globalScope);
const signInOptions =
Array.isArray(publicSettings.signInOptions) &&
publicSettings.signInOptions.length > 0
? publicSettings.signInOptions
: ["google"];
const uiConfig: firebaseui.auth.Config = {
...defaultUiConfig,
...props.uiConfig,
callbacks: {
uiShown: () => {
const node = document.getElementById("rowy-firebaseui-skeleton");
if (node) node.style.display = "none";
},
...props.uiConfig?.callbacks,
},
signInOptions: getSignInOptions(signInOptions),
};
return (
<>
<Typography variant="button" className={classes.signInText}>
Continue with
</Typography>
<StyledFirebaseAuth
{...props}
firebaseAuth={firebaseAuth}
uiConfig={uiConfig}
className={cx(classes.root, props.className)}
/>
</>
);
}

10
src/config/dbPaths.ts Normal file
View File

@@ -0,0 +1,10 @@
export const CONFIG = "_rowy_" as const;
export const SETTINGS = `${CONFIG}/settings` as const;
export const PUBLIC_SETTINGS = `${CONFIG}/publicSettings` as const;
export const TABLE_SCHEMAS = `${SETTINGS}/schema` as const;
export const TABLE_GROUP_SCHEMAS = `${SETTINGS}/groupSchema` as const;
export const USER_MANAGEMENT = `${CONFIG}/userManagement` as const;
export const USERS = `${USER_MANAGEMENT}/users` as const;

67
src/config/firebaseui.ts Normal file
View File

@@ -0,0 +1,67 @@
import {
GoogleAuthProvider,
TwitterAuthProvider,
FacebookAuthProvider,
GithubAuthProvider,
EmailAuthProvider,
PhoneAuthProvider,
} from "firebase/auth";
import * as firebaseui from "firebaseui";
import twitterLogo from "@src/assets/logos/twitter.svg";
import facebookLogo from "@src/assets/logos/facebook.svg";
import githubLogo from "@src/assets/logos/github.svg";
import appleLogo from "@src/assets/logos/apple.svg";
import yahooLogo from "@src/assets/logos/yahoo.svg";
export const authOptions = {
google: {
provider: GoogleAuthProvider.PROVIDER_ID,
},
twitter: {
provider: TwitterAuthProvider.PROVIDER_ID,
iconUrl: twitterLogo,
},
facebook: {
provider: FacebookAuthProvider.PROVIDER_ID,
iconUrl: facebookLogo,
},
github: {
provider: GithubAuthProvider.PROVIDER_ID,
iconUrl: githubLogo,
},
microsoft: {
provider: "microsoft.com",
loginHintKey: "login_hint",
},
apple: {
provider: "apple.com",
iconUrl: appleLogo,
},
yahoo: {
provider: "yahoo.com",
iconUrl: yahooLogo,
},
email: {
provider: EmailAuthProvider.PROVIDER_ID,
requireDisplayName: true,
disableSignUp: { status: true },
},
phone: {
provider: PhoneAuthProvider.PROVIDER_ID,
},
anonymous: {
provider: firebaseui.auth.AnonymousAuthProvider.PROVIDER_ID,
},
};
export const defaultUiConfig: firebaseui.auth.Config = {
signInFlow: "popup",
signInSuccessUrl: "/",
signInOptions: [authOptions.google],
};
export const getSignInOptions = (
selected: Array<keyof typeof authOptions>
): firebaseui.auth.Config["signInOptions"] =>
selected.map((option) => authOptions[option]);

View File

@@ -0,0 +1,55 @@
import { CONFIG, USERS, PUBLIC_SETTINGS } from "./dbPaths";
export const RULES_START = `rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
`;
export const RULES_END = `
}
}`;
export const REQUIRED_RULES = `
// Rowy: Allow signed in users to read Rowy configuration and admins to write
match /${CONFIG}/{docId} {
allow read: if request.auth != null;
allow write: if hasAnyRole(["ADMIN", "OWNER"]);
match /{document=**} {
allow read: if request.auth != null;
allow write: if hasAnyRole(["ADMIN", "OWNER"]);
}
}
// Rowy: Allow users to edit their settings
match /${USERS}/{userId} {
allow get, update, delete: if isDocOwner(userId);
allow create: if request.auth != null;
}
// Rowy: Allow public to read public Rowy configuration
match /${PUBLIC_SETTINGS} {
allow get: if true;
}
` as const;
export const ADMIN_RULES = `
// Allow admins to read and write all documents
match /{document=**} {
allow read, write: if hasAnyRole(["ADMIN", "OWNER"]);
}
` as const;
export const RULES_UTILS = `
// Rowy: Utility functions
function isDocOwner(docId) {
return request.auth != null && (request.auth.uid == resource.id || request.auth.uid == docId);
}
function hasAnyRole(roles) {
return request.auth != null && request.auth.token.roles.hasAny(roles);
}
` as const;
export const INSECURE_RULES = `
match /{document=**} {
allow read, write: if true;
}
` as const;

View File

@@ -0,0 +1,16 @@
export const RULES_START = `rules_version = '2';
service firebase.storage {
match /b/{bucket}/o {
`;
export const RULES_END = `
}
}`;
export const REQUIRED_RULES = `
// Rowy: Allow signed in users with Roles to read and write to Storage
match /{allPaths=**} {
allow read, write: if request.auth.token.roles.size() > 0;
}
`;

3
src/constants/dates.tsx Normal file
View File

@@ -0,0 +1,3 @@
export const DATE_FORMAT = "yyyy-MM-dd";
export const TIME_FORMAT = "HH:mm";
export const DATE_TIME_FORMAT = DATE_FORMAT + " " + TIME_FORMAT;

View File

@@ -0,0 +1,59 @@
import _mapValues from "lodash/mapValues";
import meta from "@root/package.json";
export const EXTERNAL_LINKS = {
homepage: meta.homepage,
privacy: meta.homepage + "/privacy",
terms: meta.homepage + "/terms",
docs: meta.homepage.replace("//", "//docs."),
gitHub: meta.repository.url.replace(".git", ""),
discord: "https://discord.gg/B8yAD5PDX4",
twitter: "https://twitter.com/rowyio",
rowyRun: meta.repository.url.replace(".git", "Run"),
rowyRunGitHub: meta.repository.url.replace(".git", "Run"),
// prettier-ignore
rowyRunDeploy: `https://deploy.cloud.run/?git_repo=${meta.repository.url.replace(".git", "Run")}.git`,
rowyAppHostName: "rowy.app",
dateFormat: "https://date-fns.org/v2.24.0/docs/format",
};
const WIKI_PATHS = {
setup: "/setup/install",
setupFirebaseProject: "/setup/firebase-project",
setupRoles: "/setup/roles",
setupUpdate: "/setup/update",
howToCreateTable: "/how-to/create-table",
howToCreateColumn: "/how-to/create-column",
howToAddRow: "/how-to/add-row",
howToDefaultValues: "/how-to/default-values",
howToCustomViews: "/how-to/custom-views",
fieldTypesSupportedFields: "/field-types/supported-fields",
fieldTypesDerivative: "/field-types/derivative",
fieldTypesConnectTable: "/field-types/connect-table",
fieldTypesConnector: "/field-types/connector",
fieldTypesConnectService: "/field-types/connect-service",
fieldTypesAction: "/field-types/action",
fieldTypesAdd: "/field-types/add",
rowyRun: "/rowy-run",
extensions: "/extensions",
extensionsDocSync: "/extensions/doc-sync",
extensionsAlgoliaIndex: "/extensions/algolia-index",
extensionsSlackMessage: "/extensions/slack-message",
extensionsSendgridEmail: "/extensions/sendgrid-email",
extensionsTwilioMessage: "/extensions/twilio-message",
webhooks: "/webhooks",
};
export const WIKI_LINKS = _mapValues(
WIKI_PATHS,
(path) => EXTERNAL_LINKS.docs + path
);
export const EMAIL_REQUEST = `mailto:hello@rowy.io?subject=Feature%20request%3A%20Webhooks%2FExtension%2FOther&body=**Please%20describe%20the%20problem%20you%20are%20trying%20to%20solve%3A**%0D%0A(Please%20provide%20as%20much%20information%20as%20you%20can%20to%20help%20us%20address%20faster)%0D%0A%0D%0A%0D%0A**Describe%20the%20solution%20you%E2%80%99d%20like%3A**%0D%0A%0D%0A%0D%0A**Optionally%2C%20describe%20how%20you%20currently%20solve%20this%20problem%20or%20any%20alternatives%20that%20you've%20considered%3A**%0D%0A%0D%0A%0D%0A**Optionally%2C%20additional%20context%3A**%0D%0A(Add%20any%20other%20context%2C%20screenshots%2C%20or%20screen%20recordings)%0D%0A%0D%0A`;

49
src/constants/fields.ts Normal file
View File

@@ -0,0 +1,49 @@
// Define field type strings used in column config
export enum FieldType {
// TEXT
shortText = "SIMPLE_TEXT",
longText = "LONG_TEXT",
richText = "RICH_TEXT",
email = "EMAIL",
phone = "PHONE_NUMBER",
url = "URL",
// SELECT
singleSelect = "SINGLE_SELECT",
multiSelect = "MULTI_SELECT",
// NUMERIC
checkbox = "CHECK_BOX",
number = "NUMBER",
percentage = "PERCENTAGE",
rating = "RATING",
slider = "SLIDER",
color = "COLOR",
// DATE & TIME
date = "DATE",
dateTime = "DATE_TIME",
duration = "DURATION",
// FILE
image = "IMAGE",
file = "FILE",
// CONNECTION
subTable = "SUB_TABLE",
connector = "CONNECTOR",
connectTable = "DOCUMENT_SELECT",
connectService = "SERVICE_SELECT",
// CODE
json = "JSON",
code = "CODE",
// CLOUD FUNCTION
action = "ACTION",
derivative = "DERIVATIVE",
aggregate = "AGGREGATE",
status = "STATUS",
// AUDIT
createdBy = "CREATED_BY",
updatedBy = "UPDATED_BY",
createdAt = "CREATED_AT",
updatedAt = "UPDATED_AT",
// METADATA
user = "USER",
id = "ID",
last = "LAST",
}

26
src/constants/routes.ts Normal file
View File

@@ -0,0 +1,26 @@
export enum routes {
home = "/",
auth = "/auth",
impersonatorAuth = "/impersonatorAuth",
jwtAuth = "/jwtAuth",
signOut = "/signOut",
signUp = "/signUp",
authSetup = "/authSetup",
setup = "/setup",
pageNotFound = "/404",
table = "/table",
tableWithId = "/table/:id",
tableGroup = "/tableGroup",
tableGroupWithId = "/tableGroup/:id",
settings = "/settings",
userSettings = "/settings/user",
projectSettings = "/settings/project",
userManagement = "/settings/userManagement",
rowyRunTest = "/rrTest",
}
export default routes;

View File

@@ -0,0 +1,62 @@
export type RunRoute = {
path: string;
method: "POST" | "GET" | "DELETE";
};
type impersonateUserRequest = {
path: "/impersonateUser";
method: "GET";
params: string[];
};
type ActionData = {
ref: {
id: string;
path: string;
parentId: string;
tablePath: string;
};
schemaDocPath?: string;
column: any;
action: "run" | "redo" | "undo";
actionParams: any;
};
type actionScriptRequest = {
path: "/actionScript";
method: "POST";
body: ActionData;
};
export type runRouteRequest = actionScriptRequest | impersonateUserRequest;
export const runRoutes = {
impersonateUser: { path: "/impersonateUser", method: "GET" } as RunRoute,
version: { path: "/version", method: "GET" } as RunRoute,
region: { path: "/region", method: "GET" } as RunRoute,
firestoreRules: { path: "/firestoreRules", method: "GET" } as RunRoute,
setFirestoreRules: { path: "/setFirestoreRules", method: "POST" } as RunRoute,
listCollections: { path: "/listCollections", method: "GET" } as RunRoute,
listSecrets: { path: "/listSecrets", method: "GET" } as RunRoute,
serviceAccountAccess: {
path: "/serviceAccountAccess",
method: "GET",
} as RunRoute,
checkFT2Rowy: { path: "/checkFT2Rowy", method: "GET" } as RunRoute,
migrateFT2Rowy: { path: "/migrateFT2Rowy", method: "GET" } as RunRoute,
actionScript: { path: "/actionScript", method: "POST" } as RunRoute,
buildFunction: { path: "/buildFunction", method: "POST" } as RunRoute,
publishWebhooks: { path: "/publish", method: "POST" } as RunRoute,
projectOwner: { path: "/projectOwner", method: "GET" } as RunRoute,
setOwnerRoles: { path: "/setOwnerRoles", method: "GET" } as RunRoute,
inviteUser: { path: "/inviteUser", method: "POST" } as RunRoute,
setUserRoles: { path: "/setUserRoles", method: "POST" } as RunRoute,
deleteUser: { path: "/deleteUser", method: "DELETE" } as RunRoute,
algoliaSearchKey: { path: `/algoliaSearchKey`, method: "GET" } as RunRoute,
algoliaAppId: { path: `/algoliaAppId`, method: "GET" } as RunRoute,
functionLogs: { path: `/functionLogs`, method: "GET" } as RunRoute,
auditChange: { path: `/auditChange`, method: "POST" } as RunRoute,
evaluateDerivative: {
path: `/evaluateDerivative`,
method: "POST",
} as RunRoute,
} as const;

View File

@@ -0,0 +1,88 @@
import {
SnackbarProvider as NotistackProvider,
SnackbarProviderProps,
} from "notistack";
import { makeStyles } from "tss-react/mui";
import { Grow } from "@mui/material";
import ErrorIcon from "@mui/icons-material/ErrorOutline";
import InfoIcon from "@mui/icons-material/InfoOutlined";
import SuccessIcon from "@mui/icons-material/Check";
import WarningIcon from "@mui/icons-material/WarningAmber";
const useStyles = makeStyles()((theme) => ({
containerRoot: {
"&&": {
[theme.breakpoints.down("sm")]: {
maxWidth: `calc(100% - ${theme.spacing(2)})`,
},
},
"&.SnackbarContainer-top": {
top: `max(env(safe-area-inset-top), ${theme.spacing(3)})`,
[theme.breakpoints.down("sm")]: {
top: `max(env(safe-area-inset-top), ${theme.spacing(1)})`,
},
},
"&.SnackbarContainer-bottom": {
bottom: `max(env(safe-area-inset-bottom), ${theme.spacing(3)})`,
[theme.breakpoints.down("sm")]: {
bottom: `max(env(safe-area-inset-bottom), ${theme.spacing(1)})`,
},
},
"&.SnackbarContainer-right": {
right: `max(env(safe-area-inset-right), ${theme.spacing(3)})`,
[theme.breakpoints.down("sm")]: {
right: `max(env(safe-area-inset-right), ${theme.spacing(1)})`,
},
},
"&.SnackbarContainer-left": {
left: `max(env(safe-area-inset-left), ${theme.spacing(3)})`,
[theme.breakpoints.down("sm")]: {
left: `max(env(safe-area-inset-left), ${theme.spacing(1)})`,
},
},
},
root: {
"& .SnackbarItem-contentRoot": {
borderRadius: (theme.shape.borderRadius as number) * 1.5,
boxShadow: theme.shadows[6],
"&.SnackbarItem-variantError": {
backgroundColor: theme.palette.error.main,
color: theme.palette.error.contrastText,
},
"&.SnackbarItem-variantInfo": {
backgroundColor: theme.palette.info.main,
color: theme.palette.info.contrastText,
},
"&.SnackbarItem-variantSuccess": {
backgroundColor: theme.palette.success.main,
color: theme.palette.success.contrastText,
},
"&.SnackbarItem-variantWarning": {
backgroundColor: theme.palette.warning.main,
color: theme.palette.warning.contrastText,
},
},
},
}));
export default function SnackbarProvider(props: SnackbarProviderProps) {
const { classes } = useStyles();
return (
<NotistackProvider
TransitionComponent={Grow as any}
iconVariant={{
error: <ErrorIcon sx={{ ml: -0.75, mr: 1 }} />,
info: <InfoIcon sx={{ ml: -0.75, mr: 1 }} />,
success: <SuccessIcon sx={{ ml: -0.75, mr: 1 }} />,
warning: <WarningIcon sx={{ ml: -0.75, mr: 1 }} />,
}}
{...props}
classes={{ ...classes, ...props.classes }}
/>
);
}

View File

@@ -12,44 +12,65 @@ import {
import { globalScope } from "@src/atoms/globalScope";
import { firebaseDbAtom } from "@src/sources/ProjectSourceFirebase";
/** Options for {@link useFirestoreDocWithAtom} */
interface IUseFirestoreDocWithAtomOptions {
/** Additional path segments appended to the path. If any are undefined, the listener isnt created at all. */
pathSegments?: Array<string | undefined>;
/** Called when an error occurs. Make sure to wrap in useCallback! */
onError?: (error: FirestoreError) => void;
/** Optionally disable Suspense */
disableSuspense?: boolean;
}
/**
* Attaches a listener for Firestore documents and unsubscribes on unmount.
* Gets the Firestore instance initiated in globalScope.
* Updates an atom and suspends that atom until the first snapshot is received.
* Updates an atom and optionally Suspends that atom until the first snapshot
* is received.
*
* @param dataAtom - Atom to store data in
* @param dataScope - Atom scope
* @param path - Document path
* @param pathSegments - Additional path segments appended to the path
* @param onError - Called when an error occurs. Make sure to wrap in useCallback!
* @param path - Document path. If falsy, the listener isnt created at all.
* @param options - {@link IUseFirestoreDocWithAtomOptions}
*/
export default function useFirestoreDocWithAtom(
dataAtom: PrimitiveAtom<DocumentData>,
dataScope: Scope | undefined,
path: string | undefined,
pathSegments?: Array<string | undefined>,
onError?: (error: FirestoreError) => void
options?: IUseFirestoreDocWithAtomOptions
) {
const [firebaseDb] = useAtom(firebaseDbAtom, globalScope);
const setDataAtom = useUpdateAtom(dataAtom, dataScope);
// Destructure options so they can be used as useEffect dependencies
const { pathSegments, onError, disableSuspense } = options || {};
useEffect(() => {
if (!path || (Array.isArray(pathSegments) && pathSegments.some((x) => !x)))
return;
let suspended = false;
// Suspend data atom until we get the first snapshot
setDataAtom(new Promise(() => {}));
if (!disableSuspense) {
setDataAtom(new Promise(() => {}));
suspended = true;
}
const unsubscribe = onSnapshot(
doc(firebaseDb, path, ...((pathSegments as string[]) || [])),
(doc) => {
setDataAtom(doc.data()!);
suspended = false;
},
onError
(error) => {
if (suspended) setDataAtom({});
if (onError) onError(error);
}
);
return () => {
unsubscribe();
};
}, [firebaseDb, path, pathSegments, onError, setDataAtom]);
}, [firebaseDb, path, pathSegments, onError, setDataAtom, disableSuspense]);
}

View File

@@ -1,15 +1,44 @@
import { StrictMode } from "react";
import { createRoot } from "react-dom/client";
import { ErrorBoundary } from "react-error-boundary";
import ErrorFallback from "@src/components/ErrorFallback";
import { BrowserRouter } from "react-router-dom";
import { HelmetProvider } from "react-helmet-async";
import { Provider } from "jotai";
import { globalScope } from "@src/atoms/globalScope";
import createCache from "@emotion/cache";
import { CacheProvider } from "@emotion/react";
import ThemeProvider from "@src/theme/ThemeProvider";
import SnackbarProvider from "@src/contexts/SnackbarContext";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
export const muiCache = createCache({ key: "mui", prepend: true });
const container = document.getElementById("root")!;
const root = createRoot(container);
root.render(
<StrictMode>
<App />
</StrictMode>
// <StrictMode>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<BrowserRouter>
<HelmetProvider>
<Provider scope={globalScope}>
<CacheProvider value={muiCache}>
<ThemeProvider>
<ErrorBoundary FallbackComponent={ErrorFallback}>
<SnackbarProvider>
<App />
</SnackbarProvider>
</ErrorBoundary>
</ThemeProvider>
</CacheProvider>
</Provider>
</HelmetProvider>
</BrowserRouter>
</ErrorBoundary>
// </StrictMode>
);
// If you want to start measuring performance in your app, pass a function

173
src/layouts/AuthLayout.tsx Normal file
View File

@@ -0,0 +1,173 @@
import { useAtom } from "jotai";
import {
Paper,
Typography,
LinearProgress,
Stack,
Link,
LinkProps,
} from "@mui/material";
import { alpha, Theme } from "@mui/material/styles";
import BrandedBackground, { Wrapper } from "@src/assets/BrandedBackground";
import Logo from "@src/assets/Logo";
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
import { globalScope } from "@src/atoms/globalScope";
import { projectIdAtom } from "@src/atoms/project";
export interface IAuthLayoutProps {
hideLogo?: boolean;
hideProject?: boolean;
hideLinks?: boolean;
title?: React.ReactNode;
description?: React.ReactNode;
children?: React.ReactNode;
loading?: boolean;
}
export default function AuthLayout({
hideLogo,
hideProject,
hideLinks,
title,
description,
children,
loading,
}: IAuthLayoutProps) {
const [projectId] = useAtom(projectIdAtom, globalScope);
const linkProps: LinkProps = {
variant: "caption",
color: "text.secondary",
underline: "hover",
target: "_blank",
rel: "noopener noreferrer",
};
return (
<Wrapper sx={hideLogo ? { gap: (theme) => theme.spacing(2) } : {}}>
<BrandedBackground />
<div
style={{
textAlign: "center",
marginBottom: -8,
display: hideLogo && hideLinks ? "none" : "block",
visibility: hideLogo ? "hidden" : "visible",
}}
>
<a
href={EXTERNAL_LINKS.homepage}
target="_blank"
rel="noopener noreferrer"
>
<Logo />
</a>
</div>
<Paper
component="main"
elevation={4}
sx={
{
position: "relative",
overflow: "hidden",
maxWidth: 360,
width: "100%",
px: 4,
py: 3,
minHeight: 300,
backgroundColor: (theme: Theme) =>
alpha(theme.palette.background.paper, 0.5),
backdropFilter: "blur(20px) saturate(150%)",
display: "flex",
flexDirection: "column",
textAlign: "center",
"& > :not(style) + :not(style)": { mt: 6 },
} as any
}
>
{title && (
<Typography component="h1" variant="h4">
{title}
</Typography>
)}
{description && (
<Typography variant="body1" style={{ marginTop: 8 }}>
{description}
</Typography>
)}
<Stack
spacing={4}
justifyContent="center"
alignItems="center"
style={{ textAlign: "center", flexGrow: 1 }}
>
{children}
</Stack>
{loading && (
<LinearProgress
style={{
position: "absolute",
left: 0,
right: 0,
top: 0,
marginTop: 0,
}}
/>
)}
<Typography
variant="caption"
color="text.secondary"
sx={{ pt: 1, display: hideProject ? "none" : "block" }}
>
Project: <span style={{ userSelect: "all" }}>{projectId}</span>
</Typography>
</Paper>
<Stack
spacing={{ xs: 1.25, sm: 2 }}
direction="row"
flexWrap="wrap"
justifyContent="center"
style={{
maxWidth: 360,
width: "100%",
padding: "0 4px",
display: hideLogo && hideLinks ? "none" : "flex",
visibility: hideLinks ? "hidden" : "visible",
}}
>
<Link href={EXTERNAL_LINKS.homepage} {...linkProps}>
{EXTERNAL_LINKS.homepage.split("//").pop()?.replace(/\//g, "")}
</Link>
<Link href={EXTERNAL_LINKS.discord} {...linkProps}>
Discord
</Link>
<Link href={EXTERNAL_LINKS.twitter} {...linkProps}>
Twitter
</Link>
<div style={{ flexGrow: 1, marginLeft: 0 }} />
<Link href={EXTERNAL_LINKS.docs} {...linkProps}>
Docs
</Link>
<Link href={EXTERNAL_LINKS.privacy} {...linkProps}>
Privacy
</Link>
<Link href={EXTERNAL_LINKS.terms} {...linkProps}>
Terms
</Link>
</Stack>
</Wrapper>
);
}

View File

@@ -0,0 +1,94 @@
import { Stack, Paper, Typography, Button } from "@mui/material";
import { alpha } from "@mui/material/styles";
import DiscordIcon from "@src/assets/icons/Discord";
import TwitterIcon from "@mui/icons-material/Twitter";
import Logo from "@src/assets/Logo";
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
export default function Marketing() {
return (
<Paper
elevation={4}
square
sx={{
display: { xs: "none", md: "block" },
width: 520,
gridColumn: 1,
gridRow: "1 / 4",
backgroundColor: (theme) => alpha(theme.palette.background.paper, 0.5),
backdropFilter: "blur(20px) saturate(150%)",
pt: (theme) => `max(env(safe-area-inset-top), ${theme.spacing(8)})`,
pb: (theme) => `max(env(safe-area-inset-bottom), ${theme.spacing(8)})`,
pl: (theme) => `max(env(safe-area-inset-left), ${theme.spacing(8)})`,
pr: 8,
}}
>
<Stack
direction="column"
justifyContent="space-between"
spacing={4}
style={{ height: "100%" }}
>
<a
href={EXTERNAL_LINKS.homepage}
target="_blank"
rel="noopener noreferrer"
>
<Logo size={2} />
</a>
<div>
<Typography
component="p"
variant="h5"
sx={{ fontWeight: "normal", fontSize: 28 / 16 + "rem" }}
paragraph
>
Manage Firestore data in a spreadsheet-like&nbsp;UI
</Typography>
<Typography
component="p"
variant="h5"
sx={{ fontWeight: "normal", fontSize: 28 / 16 + "rem" }}
paragraph
>
Write Cloud Functions effortlessly&nbsp;in the browser
</Typography>
<Typography
component="p"
variant="h5"
sx={{ fontWeight: "normal", fontSize: 28 / 16 + "rem" }}
paragraph
>
Connect to your favorite third&nbsp;party platforms
</Typography>
</div>
<Stack direction="row" spacing={1}>
<Button
variant="outlined"
startIcon={<DiscordIcon color="action" />}
href={EXTERNAL_LINKS.discord}
target="_blank"
rel="noopener noreferrer"
>
Join our community
</Button>
<Button
variant="outlined"
startIcon={<TwitterIcon color="action" />}
href={EXTERNAL_LINKS.twitter}
target="_blank"
rel="noopener noreferrer"
>
Follow on Twitter
</Button>
</Stack>
</Stack>
</Paper>
);
}

13
src/layouts/Nav.tsx Normal file
View File

@@ -0,0 +1,13 @@
import { Outlet } from "react-router-dom";
import { Container, Typography } from "@mui/material";
export interface INavProps {}
export default function Nav(props: INavProps) {
return (
<Container>
<Typography variant="h1">Nav</Typography>
<Outlet />
</Container>
);
}

View File

@@ -0,0 +1,33 @@
import { useAtom } from "jotai";
import { useLocation, Navigate } from "react-router-dom";
import Loading from "@src/components/Loading";
import { globalScope } from "@src/atoms/globalScope";
import { currentUserAtom } from "@src/atoms/auth";
import routes from "constants/routes";
export interface IRequireAuthProps {
children: React.ReactElement;
}
export default function RequireAuth({ children }: IRequireAuthProps) {
const [currentUser] = useAtom(currentUserAtom, globalScope);
const location = useLocation();
if (currentUser === undefined)
return <Loading fullScreen message="Authenticating" />;
const redirect =
(location.pathname ?? "") + (location.search ?? "") + (location.hash ?? "");
if (currentUser === null)
return (
<Navigate
to={routes.auth + `?redirect=${encodeURIComponent(redirect)}`}
replace
/>
);
return children;
}

View File

@@ -0,0 +1,110 @@
/* eslint-disable @typescript-eslint/no-unused-vars */
import { useEffect, useState } from "react";
import { useAtom } from "jotai";
import { useSnackbar } from "notistack";
import { signOut } from "firebase/auth";
import { Typography, Button, TextField } from "@mui/material";
import AuthLayout from "@src/layouts/AuthLayout";
// import FirebaseUi from "@src/components/Auth/FirebaseUi";
import { globalScope } from "@src/atoms/globalScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
import { runRoutes } from "@src/constants/runRoutes";
export default function ImpersonatorAuthPage() {
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
const { enqueueSnackbar } = useSnackbar();
// const { rowyRun } = useProjectContext();
useEffect(() => {
//sign out user on initial load
// signOut();
}, []);
const [loading, setLoading] = useState(false);
const [adminUser, setAdminUser] = useState();
const [email, setEmail] = useState("");
const handleAuth = async (email: string) => {
// if (!rowyRun) return;
// setLoading(true);
// const resp = await rowyRun({
// route: runRoutes.impersonateUser,
// params: [email],
// });
// setLoading(false);
// if (resp.success) {
// enqueueSnackbar(resp.message, { variant: "success" });
// await auth.signInWithCustomToken(resp.token);
// window.location.href = "/";
// } else {
// enqueueSnackbar(resp.error.message, { variant: "error" });
// }
};
return (
<AuthLayout
loading={loading}
title="Admin auth"
description={
<>
<Typography
variant="inherit"
component="span"
display="block"
gutterBottom
>
Using an admin account, sign in as another user on this project to
test permissions and access controls.
</Typography>
<Typography variant="inherit" component="span">
Make sure the Rowy Run service account has the{" "}
<b>Service Account Token Creator</b> IAM role.
</Typography>
</>
}
>
{/* {adminUser === undefined ? (
<FirebaseUi
uiConfig={{
callbacks: {
signInSuccessWithAuthResult: (authUser) => {
authUser.user.getIdTokenResult().then((result) => {
if (result.claims.roles?.includes("ADMIN")) {
setAdminUser(authUser.user);
} else {
enqueueSnackbar("Not an admin account", {
variant: "error",
});
signOut();
}
});
return false;
},
},
}}
/>
) : (
<>
<TextField
name="email"
label="Email"
fullWidth
autoFocus
onChange={(e) => setEmail(e.target.value)}
/>
<Button
variant="contained"
disabled={email === ""}
onClick={() => handleAuth(email)}
>
Sign in
</Button>
</>
)} */}
</AuthLayout>
);
}

View File

@@ -0,0 +1,48 @@
import { useState } from "react";
import { useAtom } from "jotai";
import { useSnackbar } from "notistack";
import { signInWithCustomToken } from "firebase/auth";
import { TextField, Button } from "@mui/material";
import AuthLayout from "@src/layouts/AuthLayout";
import { globalScope } from "@src/atoms/globalScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
export default function JwtAuthPage() {
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
const { enqueueSnackbar } = useSnackbar();
const [jwt, setJWT] = useState("");
const [loading, setLoading] = useState(false);
const handleAuth = async () => {
setLoading(true);
try {
await signInWithCustomToken(firebaseAuth, jwt);
enqueueSnackbar("Success", { variant: "success" });
window.location.assign("/");
} catch (e: any) {
enqueueSnackbar(e.message, { variant: "error" });
} finally {
setLoading(false);
}
};
return (
<AuthLayout loading={loading} title="Test auth">
<TextField
name="JWT"
label="JWT"
autoFocus
fullWidth
onChange={(e) => setJWT(e.target.value)}
/>
<Button variant="contained" disabled={jwt === ""} onClick={handleAuth}>
Sign in
</Button>
</AuthLayout>
);
}

View File

@@ -0,0 +1,27 @@
import { useEffect } from "react";
import { useAtom } from "jotai";
import { Link } from "react-router-dom";
import { signOut } from "firebase/auth";
import { Button } from "@mui/material";
import AuthLayout from "@src/layouts/AuthLayout";
import { globalScope } from "@src/atoms/globalScope";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
export default function SignOutPage() {
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
useEffect(() => {
signOut(firebaseAuth);
}, [firebaseAuth]);
return (
<AuthLayout title="Signed out">
<Button component={Link} to="/auth" variant="outlined">
Sign in again
</Button>
</AuthLayout>
);
}

76
src/pages/Auth/SignUp.tsx Normal file
View File

@@ -0,0 +1,76 @@
import { useSearchParams } from "react-router-dom";
import { useMediaQuery, Stack, Typography, Link } from "@mui/material";
import MarketingPanel from "@src/layouts/MarketingPanel";
import AuthLayout from "@src/layouts/AuthLayout";
import FirebaseUi from "@src/components/FirebaseUi";
import { EXTERNAL_LINKS } from "@src/constants/externalLinks";
export default function SignUpPage() {
const [searchParams] = useSearchParams();
const uiConfig: firebaseui.auth.Config = {};
const redirect = searchParams.get("redirect");
if (typeof redirect === "string" && redirect.length > 0) {
uiConfig.signInSuccessUrl = redirect;
}
const isMobile = useMediaQuery((theme: any) => theme.breakpoints.down("md"));
return (
<Stack direction="row">
<MarketingPanel />
<div style={{ flexGrow: 1 }}>
<AuthLayout
hideLogo={!isMobile}
hideLinks={!isMobile}
title="Sign up"
description={
<>
Welcome! To join this project, sign in with the email address
{searchParams.get("email") ? (
<>
:{" "}
<b style={{ userSelect: "all" }}>
{searchParams.get("email")}
</b>
</>
) : (
" used to invite you."
)}
</>
}
>
<FirebaseUi uiConfig={uiConfig} />
<Typography
variant="caption"
color="text.secondary"
style={{ marginTop: 16 }}
>
By signing up, you agree to our{" "}
<Link
href={EXTERNAL_LINKS.terms}
target="_blank"
rel="noopener noreferrer"
color="text.secondary"
>
Terms and Conditions
</Link>{" "}
and{" "}
<Link
href={EXTERNAL_LINKS.privacy}
target="_blank"
rel="noopener noreferrer"
color="text.secondary"
>
Privacy Policy
</Link>
.
</Typography>
</AuthLayout>
</div>
</Stack>
);
}

20
src/pages/Auth/index.tsx Normal file
View File

@@ -0,0 +1,20 @@
import { useSearchParams } from "react-router-dom";
import AuthLayout from "@src/layouts/AuthLayout";
import FirebaseUi from "@src/components/FirebaseUi";
export default function AuthPage() {
const [searchParams] = useSearchParams();
const uiConfig: firebaseui.auth.Config = {};
const redirect = searchParams.get("redirect");
if (typeof redirect === "string" && redirect.length > 0) {
uiConfig.signInSuccessUrl = redirect;
}
return (
<AuthLayout title="Sign in">
<FirebaseUi uiConfig={uiConfig} />
</AuthLayout>
);
}

View File

@@ -8,6 +8,7 @@ import { publicSettingsAtom } from "@src/atoms/project";
// import { GoogleAuthProvider } from "firebase/auth";
import { firebaseAuthAtom } from "@src/sources/ProjectSourceFirebase";
import { Button } from "@mui/material";
import { useSnackbar } from "notistack";
import {
GoogleAuthProvider,
@@ -32,6 +33,8 @@ function Auth() {
console.log("publicSettings", publicSettings);
console.log("userSettings", userSettings);
const { enqueueSnackbar } = useSnackbar();
return (
<>
<Button
@@ -39,6 +42,7 @@ function Auth() {
color={currentUser ? "secondary" : "primary"}
onClick={() => {
signInWithPopup(firebaseAuth, provider);
enqueueSnackbar("Signed in");
}}
sx={{ my: 4, mx: 1 }}
>
@@ -49,6 +53,7 @@ function Auth() {
color={!currentUser ? "secondary" : "primary"}
onClick={() => {
signOut(firebaseAuth);
enqueueSnackbar("Signed out");
}}
sx={{ my: 4, mx: 1 }}
>

56
src/pages/NotFound.tsx Normal file
View File

@@ -0,0 +1,56 @@
import { useAtom } from "jotai";
import { Link } from "react-router-dom";
import { Button } from "@mui/material";
import GoIcon from "@src/assets/icons/Go";
import HomeIcon from "@mui/icons-material/HomeOutlined";
// import AuthLayout from "@src/components/Auth/AuthLayout";
// import Navigation, { APP_BAR_HEIGHT } from "@src/components/Navigation";
import EmptyState from "@src/components/EmptyState";
import meta from "@root/package.json";
import routes from "@src/constants/routes";
import { globalScope } from "@src/atoms/globalScope";
import { currentUserAtom } from "@src/atoms/auth";
export default function NotFound() {
const [currentUser] = useAtom(currentUserAtom, globalScope);
// if (currentUser === undefined) throw new Promise(() => {});
if (!currentUser)
return (
// <AuthLayout title="Page not found">
<Button
variant="outlined"
sx={{ mt: 3 }}
href={meta.homepage}
endIcon={<GoIcon style={{ margin: "0 -0.33em" }} />}
>
{meta.homepage.split("//")[1].replace(/\//g, "")}
</Button>
// </AuthLayout>
);
return (
// <Navigation title="Page not found" titleComponent={() => <div />}>
<EmptyState
message="Page not found"
description={
<Button
variant="outlined"
sx={{ mt: 3 }}
component={Link}
to={routes.home}
startIcon={<HomeIcon />}
>
Home
</Button>
}
fullScreen
// style={{ marginTop: -APP_BAR_HEIGHT }}
/>
// </Navigation>
);
}

View File

@@ -9,7 +9,7 @@ import { currentUserAtom, userRolesAtom } from "@src/atoms/auth";
import useFirestoreDocWithAtom from "@src/hooks/useFirestoreDocWithAtom";
import { globalScope } from "@src/atoms/globalScope";
import { publicSettingsAtom } from "@src/atoms/project";
import { projectIdAtom, publicSettingsAtom } from "@src/atoms/project";
import { useUpdateAtom } from "jotai/utils";
import { userSettingsAtom } from "@src/atoms/user";
@@ -48,6 +48,13 @@ export const firebaseDbAtom = atom((get) => {
});
export default function ProjectSourceFirebase() {
// Set projectId from Firebase project
const [firebaseConfig] = useAtom(firebaseConfigAtom, globalScope);
const setProjectId = useUpdateAtom(projectIdAtom, globalScope);
useEffect(() => {
setProjectId(firebaseConfig.projectId || "");
}, [firebaseConfig.projectId, setProjectId]);
// Get current user and store in atoms
const [firebaseAuth] = useAtom(firebaseAuthAtom, globalScope);
// const setCurrentUser: any = useUpdateAtom(currentUserAtom, globalScope);
@@ -86,7 +93,7 @@ export default function ProjectSourceFirebase() {
userSettingsAtom,
globalScope,
`_rowy_/userManagement/users`,
[currentUser?.uid]
{ pathSegments: [currentUser?.uid] }
);
return null;

View File

@@ -1,11 +1,13 @@
import { useEffect } from "react";
import { useAtom } from "jotai";
import { Helmet } from "react-helmet-async";
import {
useMediaQuery,
ThemeProvider as MuiThemeProvider,
CssBaseline,
} from "@mui/material";
import Favicon from "@src/assets/Favicon";
import { globalScope } from "@src/atoms/globalScope";
import {
@@ -41,17 +43,20 @@ export default function ThemeProvider({
document.body.setAttribute("data-theme", theme);
}, [theme]);
const fontCssUrls = customizedThemes[theme].typography.fontCssUrls;
return (
<>
{Array.isArray(customizedThemes[theme].typography.fontCssUrls) && (
{Array.isArray(fontCssUrls) && (
<Helmet>
{customizedThemes[theme].typography.fontCssUrls!.map((url) => (
{fontCssUrls!.map((url) => (
<link key={url} rel="stylesheet" href={url} />
))}
</Helmet>
)}
<MuiThemeProvider theme={customizedThemes[theme]}>
<Favicon />
<CssBaseline />
{children}
</MuiThemeProvider>

View File

@@ -19,6 +19,9 @@
"noImplicitReturns": true,
"noUnusedLocals": false,
"noUnusedParameters": false,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"jsx": "react-jsx",
"baseUrl": "src"
},

167
yarn.lock
View File

@@ -1719,7 +1719,7 @@
core-js-pure "^3.20.2"
regenerator-runtime "^0.13.4"
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.3.1", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
"@babel/runtime@^7.10.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.16.3", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7":
version "7.17.9"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.9.tgz#d19fbf802d01a8cb6cf053a64e472d42c434ba72"
integrity sha512-lSiBBvodq29uShpWGNbgFdKYNiFDo5/HIYsaCEY9ff4sb10x9jizo2+pRrSyF4jKZCXqgzuqBOQKbUm90gQwJg==
@@ -1950,7 +1950,7 @@
source-map "^0.5.7"
stylis "4.0.13"
"@emotion/cache@^11.7.1":
"@emotion/cache@*", "@emotion/cache@^11.7.1":
version "11.7.1"
resolved "https://registry.yarnpkg.com/@emotion/cache/-/cache-11.7.1.tgz#08d080e396a42e0037848214e8aa7bf879065539"
integrity sha512-r65Zy4Iljb8oyjtLeCuBH8Qjiy107dOYC6SJq7g7GV5UCQWMObY4SJDPGFjiiVpPrOJ2hmJOoBiYTC7hwx9E2A==
@@ -1991,10 +1991,10 @@
"@emotion/weak-memoize" "^0.2.5"
hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
"@emotion/serialize@*", "@emotion/serialize@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.3.tgz#99e2060c26c6292469fb30db41f4690e1c8fea63"
integrity sha512-2mSSvgLfyV3q+iVh3YWgNlUc2a9ZlDU7DjuP5MjK3AXRR0dYigCrP99aeFtaB2L/hjfEZdSThn5dsZ0ufqbvsA==
dependencies:
"@emotion/hash" "^0.8.0"
"@emotion/memoize" "^0.7.4"
@@ -2002,10 +2002,10 @@
"@emotion/utils" "^1.0.0"
csstype "^3.0.2"
"@emotion/serialize@^1.0.3":
version "1.0.3"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.3.tgz#99e2060c26c6292469fb30db41f4690e1c8fea63"
integrity sha512-2mSSvgLfyV3q+iVh3YWgNlUc2a9ZlDU7DjuP5MjK3AXRR0dYigCrP99aeFtaB2L/hjfEZdSThn5dsZ0ufqbvsA==
"@emotion/serialize@^1.0.2":
version "1.0.2"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.0.2.tgz#77cb21a0571c9f68eb66087754a65fa97bfcd965"
integrity sha512-95MgNJ9+/ajxU7QIAruiOAdYNjxZX7G2mhgrtDWswA21VviYIRP1R5QilZ/bDY42xiKsaktP4egJb3QdYQZi1A==
dependencies:
"@emotion/hash" "^0.8.0"
"@emotion/memoize" "^0.7.4"
@@ -2034,16 +2034,16 @@
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.7.5.tgz#77211291c1900a700b8a78cfafda3160d76949ed"
integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
"@emotion/utils@*", "@emotion/utils@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf"
integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==
"@emotion/utils@^1.0.0":
version "1.0.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.0.0.tgz#abe06a83160b10570816c913990245813a2fd6af"
integrity sha512-mQC2b3XLDs6QCW+pDQDiyO/EdGZYOygE8s5N5rrzjSI4M3IejPE/JPndCBwRT9z982aqQNi6beWs1UeayrQxxA==
"@emotion/utils@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@emotion/utils/-/utils-1.1.0.tgz#86b0b297f3f1a0f2bdb08eeac9a2f49afd40d0cf"
integrity sha512-iRLa/Y4Rs5H/f2nimczYmS5kFJEbpiVvgN3XVfZ022IYhuNA1IRSHEizcof88LtCTXtl9S2Cxt32KgaXEu72JQ==
"@emotion/weak-memoize@^0.2.5":
version "0.2.5"
resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz#8eed982e2ee6f7f4e44c253e12962980791efd46"
@@ -2790,29 +2790,6 @@
"@emotion/cache" "^11.7.1"
prop-types "^15.7.2"
"@mui/styles@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@mui/styles/-/styles-5.6.0.tgz#c57b80974ca31c980acdd58d13b21d46fbcd81cf"
integrity sha512-1rnQ/fDQ+EtBy3eo6VI1M7EkSjeEyB4NoTaetVUGMYv9tM7qkqnr6XN7TiuB2gtAsTDfdrAABrhmYrTTTf77cw==
dependencies:
"@babel/runtime" "^7.17.2"
"@emotion/hash" "^0.8.0"
"@mui/private-theming" "^5.6.0"
"@mui/types" "^7.1.3"
"@mui/utils" "^5.6.0"
clsx "^1.1.1"
csstype "^3.0.11"
hoist-non-react-statics "^3.3.2"
jss "^10.8.2"
jss-plugin-camel-case "^10.8.2"
jss-plugin-default-unit "^10.8.2"
jss-plugin-global "^10.8.2"
jss-plugin-nested "^10.8.2"
jss-plugin-props-sort "^10.8.2"
jss-plugin-rule-value-function "^10.8.2"
jss-plugin-vendor-prefixer "^10.8.2"
prop-types "^15.7.2"
"@mui/system@^5.6.0":
version "5.6.0"
resolved "https://registry.yarnpkg.com/@mui/system/-/system-5.6.0.tgz#4d6db0db6a8daf90acd7fcaab3a353aa127987ce"
@@ -4791,7 +4768,7 @@ cliui@^7.0.2:
strip-ansi "^6.0.0"
wrap-ansi "^7.0.0"
clsx@^1.1.1:
clsx@^1.1.0, clsx@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188"
integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA==
@@ -5169,14 +5146,6 @@ css-tree@^1.1.3:
mdn-data "2.0.14"
source-map "^0.6.1"
css-vendor@^2.0.8:
version "2.0.8"
resolved "https://registry.yarnpkg.com/css-vendor/-/css-vendor-2.0.8.tgz#e47f91d3bd3117d49180a3c935e62e3d9f7f449d"
integrity sha512-x9Aq0XTInxrkuFeHKbYC7zWY8ai7qJ04Kxd9MnvbC1uO5DagxoHQjm4JvG+vCdXOoFtCjbL2XSZfxmoYa9uQVQ==
dependencies:
"@babel/runtime" "^7.8.3"
is-in-browser "^1.0.2"
css-what@^3.2.1:
version "3.4.2"
resolved "https://registry.yarnpkg.com/css-what/-/css-what-3.4.2.tgz#ea7026fcb01777edbde52124e21f327e7ae950e4"
@@ -6637,7 +6606,7 @@ history@^5.2.0:
dependencies:
"@babel/runtime" "^7.7.6"
hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
hoist-non-react-statics@^3.3.0, hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
version "3.3.2"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz#ece0acaf71d62c2969c2ec59feff42a4b1a85b45"
integrity sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==
@@ -6788,11 +6757,6 @@ husky@>=7.0.4:
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==
hyphenate-style-name@^1.0.3:
version "1.0.4"
resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d"
integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ==
iconv-lite@0.4.24:
version "0.4.24"
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
@@ -7016,11 +6980,6 @@ is-glob@^4.0.1, is-glob@^4.0.3:
dependencies:
is-extglob "^2.1.1"
is-in-browser@^1.0.2, is-in-browser@^1.1.3:
version "1.1.3"
resolved "https://registry.yarnpkg.com/is-in-browser/-/is-in-browser-1.1.3.tgz#56ff4db683a078c6082eb95dad7dc62e1d04f835"
integrity sha1-Vv9NtoOgeMYILrldrX3GLh0E+DU=
is-module@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591"
@@ -7776,76 +7735,6 @@ jsonpointer@^5.0.0:
resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.0.tgz#f802669a524ec4805fa7389eadbc9921d5dc8072"
integrity sha512-PNYZIdMjVIvVgDSYKTT63Y+KZ6IZvGRNNWcxwD+GNnUz1MKPfv30J8ueCjdwcN0nDx2SlshgyB7Oy0epAzVRRg==
jss-plugin-camel-case@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-camel-case/-/jss-plugin-camel-case-10.9.0.tgz#4921b568b38d893f39736ee8c4c5f1c64670aaf7"
integrity sha512-UH6uPpnDk413/r/2Olmw4+y54yEF2lRIV8XIZyuYpgPYTITLlPOsq6XB9qeqv+75SQSg3KLocq5jUBXW8qWWww==
dependencies:
"@babel/runtime" "^7.3.1"
hyphenate-style-name "^1.0.3"
jss "10.9.0"
jss-plugin-default-unit@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-default-unit/-/jss-plugin-default-unit-10.9.0.tgz#bb23a48f075bc0ce852b4b4d3f7582bc002df991"
integrity sha512-7Ju4Q9wJ/MZPsxfu4T84mzdn7pLHWeqoGd/D8O3eDNNJ93Xc8PxnLmV8s8ZPNRYkLdxZqKtm1nPQ0BM4JRlq2w==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-global@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-global/-/jss-plugin-global-10.9.0.tgz#fc07a0086ac97aca174e37edb480b69277f3931f"
integrity sha512-4G8PHNJ0x6nwAFsEzcuVDiBlyMsj2y3VjmFAx/uHk/R/gzJV+yRHICjT4MKGGu1cJq2hfowFWCyrr/Gg37FbgQ==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-nested@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-nested/-/jss-plugin-nested-10.9.0.tgz#cc1c7d63ad542c3ccc6e2c66c8328c6b6b00f4b3"
integrity sha512-2UJnDrfCZpMYcpPYR16oZB7VAC6b/1QLsRiAutOt7wJaaqwCBvNsosLEu/fUyKNQNGdvg2PPJFDO5AX7dwxtoA==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
tiny-warning "^1.0.2"
jss-plugin-props-sort@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-props-sort/-/jss-plugin-props-sort-10.9.0.tgz#30e9567ef9479043feb6e5e59db09b4de687c47d"
integrity sha512-7A76HI8bzwqrsMOJTWKx/uD5v+U8piLnp5bvru7g/3ZEQOu1+PjHvv7bFdNO3DwNPC9oM0a//KwIJsIcDCjDzw==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
jss-plugin-rule-value-function@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-rule-value-function/-/jss-plugin-rule-value-function-10.9.0.tgz#379fd2732c0746fe45168011fe25544c1a295d67"
integrity sha512-IHJv6YrEf8pRzkY207cPmdbBstBaE+z8pazhPShfz0tZSDtRdQua5jjg6NMz3IbTasVx9FdnmptxPqSWL5tyJg==
dependencies:
"@babel/runtime" "^7.3.1"
jss "10.9.0"
tiny-warning "^1.0.2"
jss-plugin-vendor-prefixer@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss-plugin-vendor-prefixer/-/jss-plugin-vendor-prefixer-10.9.0.tgz#aa9df98abfb3f75f7ed59a3ec50a5452461a206a"
integrity sha512-MbvsaXP7iiVdYVSEoi+blrW+AYnTDvHTW6I6zqi7JcwXdc6I9Kbm234nEblayhF38EftoenbM+5218pidmC5gA==
dependencies:
"@babel/runtime" "^7.3.1"
css-vendor "^2.0.8"
jss "10.9.0"
jss@10.9.0, jss@^10.8.2:
version "10.9.0"
resolved "https://registry.yarnpkg.com/jss/-/jss-10.9.0.tgz#7583ee2cdc904a83c872ba695d1baab4b59c141b"
integrity sha512-YpzpreB6kUunQBbrlArlsMpXYyndt9JATbt95tajx0t4MTJJcCJdd4hdNpHmOIDiUJrF/oX5wtVFrS3uofWfGw==
dependencies:
"@babel/runtime" "^7.3.1"
csstype "^3.0.2"
is-in-browser "^1.1.3"
tiny-warning "^1.0.2"
"jsx-ast-utils@^2.4.1 || ^3.0.0", jsx-ast-utils@^3.2.1:
version "3.2.2"
resolved "https://registry.yarnpkg.com/jsx-ast-utils/-/jsx-ast-utils-3.2.2.tgz#6ab1e52c71dfc0c0707008a91729a9491fe9f76c"
@@ -8384,6 +8273,14 @@ normalize-url@^6.0.1:
resolved "https://registry.yarnpkg.com/normalize-url/-/normalize-url-6.1.0.tgz#40d0885b535deffe3f3147bec877d05fe4c5668a"
integrity sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==
notistack@^2.0.4:
version "2.0.4"
resolved "https://registry.yarnpkg.com/notistack/-/notistack-2.0.4.tgz#9e043991d4788bed3d1701d1b2f694233ad38dd8"
integrity sha512-kOJmKvTG91ElMzi4aHu82BDe1liQ0zMrBp+TnWJptgowDsTbeTKbZmsRqJNIj145BmlOtZsEE9xjcrN46zVo3w==
dependencies:
clsx "^1.1.0"
hoist-non-react-statics "^3.3.0"
npm-run-path@^4.0.1:
version "4.0.1"
resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea"
@@ -10856,11 +10753,6 @@ thunky@^1.0.2:
resolved "https://registry.yarnpkg.com/thunky/-/thunky-1.1.0.tgz#5abaf714a9405db0504732bbccd2cedd9ef9537d"
integrity sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==
tiny-warning@^1.0.2:
version "1.0.3"
resolved "https://registry.yarnpkg.com/tiny-warning/-/tiny-warning-1.0.3.tgz#94a30db453df4c643d0fd566060d60a875d84754"
integrity sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==
tmp@^0.2.1:
version "0.2.1"
resolved "https://registry.yarnpkg.com/tmp/-/tmp-0.2.1.tgz#8457fc3037dcf4719c251367a1af6500ee1ccf14"
@@ -10974,6 +10866,15 @@ tslib@^2.1.0:
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.3.0.tgz#803b8cdab3e12ba581a4ca41c8839bbb0dacb09e"
integrity sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==
tss-react@^3.6.2:
version "3.6.2"
resolved "https://registry.yarnpkg.com/tss-react/-/tss-react-3.6.2.tgz#5e995d8b82ca730ba04a21203ae68f1c5372a85e"
integrity sha512-+ecQIqCNFVlVJVk3NiCWZY2+5DhVKMVUVxIbEwv9nYsTRQA/KxyKCU8g+KMj5ByqFv9LW76+TUYzbWck/IHkfA==
dependencies:
"@emotion/cache" "*"
"@emotion/serialize" "*"
"@emotion/utils" "*"
tsutils@^3.21.0:
version "3.21.0"
resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623"