add react-router, auth pages, favicon, notistack
@@ -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"
|
||||
},
|
||||
|
||||
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 15 KiB |
BIN
public/favicon/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 2.1 KiB |
BIN
public/favicon/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 5.6 KiB |
BIN
public/favicon/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
BIN
public/favicon/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 620 B |
BIN
public/favicon/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 810 B |
3
public/favicon/icon.svg
Executable 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 |
BIN
public/favicon/mstile-144x144.png
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
BIN
public/favicon/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
public/favicon/mstile-310x150.png
Normal file
|
After Width: | Height: | Size: 3.1 KiB |
BIN
public/favicon/mstile-310x310.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
public/favicon/mstile-70x70.png
Normal file
|
After Width: | Height: | Size: 1.9 KiB |
33
public/favicon/safari-pinned-tab.svg
Normal 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 |
@@ -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>
|
||||
|
||||
@@ -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
@@ -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
|
After Width: | Height: | Size: 268 KiB |
38
src/App.css
@@ -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);
|
||||
}
|
||||
}
|
||||
100
src/App.tsx
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
74
src/assets/BrandedBackground.tsx
Normal 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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
44
src/assets/LogoRowyRun.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
BIN
src/assets/SpaceGrotesk-Bold.woff2
Executable file
BIN
src/assets/SpaceGrotesk-Regular.woff2
Executable file
5
src/assets/bg-pattern-dark.svg
Normal 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 |
5
src/assets/bg-pattern.svg
Normal 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
@@ -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 |
35
src/assets/logo-sticker.svg
Normal 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 |
3
src/assets/logos/apple.svg
Normal 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 |
4
src/assets/logos/facebook.svg
Normal 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 |
3
src/assets/logos/github.svg
Normal 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 |
6
src/assets/logos/microsoft.svg
Normal 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 |
3
src/assets/logos/twitter.svg
Normal 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 |
5
src/assets/logos/yahoo.svg
Normal 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 |
@@ -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[]>([]);
|
||||
|
||||
@@ -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>({});
|
||||
|
||||
259
src/components/FirebaseUi.tsx
Normal 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
@@ -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
@@ -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]);
|
||||
55
src/config/firestoreRules.ts
Normal 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;
|
||||
16
src/config/storageRules.ts
Normal 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
@@ -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;
|
||||
59
src/constants/externalLinks.ts
Normal 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
@@ -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
@@ -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;
|
||||
62
src/constants/runRoutes.ts
Normal 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;
|
||||
88
src/contexts/SnackbarContext.tsx
Normal 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 }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -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 isn’t 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 isn’t 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]);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
94
src/layouts/MarketingPanel.tsx
Normal 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 UI
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="h5"
|
||||
sx={{ fontWeight: "normal", fontSize: 28 / 16 + "rem" }}
|
||||
paragraph
|
||||
>
|
||||
Write Cloud Functions effortlessly in the browser
|
||||
</Typography>
|
||||
<Typography
|
||||
component="p"
|
||||
variant="h5"
|
||||
sx={{ fontWeight: "normal", fontSize: 28 / 16 + "rem" }}
|
||||
paragraph
|
||||
>
|
||||
Connect to your favorite third 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
@@ -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>
|
||||
);
|
||||
}
|
||||
33
src/layouts/RequireAuth.tsx
Normal 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;
|
||||
}
|
||||
110
src/pages/Auth/ImpersonatorAuth.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
48
src/pages/Auth/JwtAuth.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
27
src/pages/Auth/SignOut.tsx
Normal 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
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
"noImplicitReturns": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noImplicitAny": true,
|
||||
"noImplicitThis": true,
|
||||
"strictNullChecks": true,
|
||||
"jsx": "react-jsx",
|
||||
"baseUrl": "src"
|
||||
},
|
||||
|
||||
167
yarn.lock
@@ -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"
|
||||
|
||||