| @@ -1 +1,4 @@ | |||||
| API_HOST=localhost | |||||
| API_HOST=localhost | |||||
| API_PORT=8090 | |||||
| API_PROTOCOL=http | |||||
| NEXTAUTH_SECRET=secret | |||||
| @@ -11,13 +11,16 @@ | |||||
| "@emotion/cache": "^11.11.0", | "@emotion/cache": "^11.11.0", | ||||
| "@emotion/react": "^11.11.1", | "@emotion/react": "^11.11.1", | ||||
| "@emotion/styled": "^11.11.0", | "@emotion/styled": "^11.11.0", | ||||
| "@fontsource/roboto": "^5.0.8", | |||||
| "@fontsource/inter": "^5.0.16", | |||||
| "@fontsource/plus-jakarta-sans": "^5.0.18", | |||||
| "@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
| "@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
| "@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
| "next": "14.0.4", | "next": "14.0.4", | ||||
| "next-auth": "^4.24.5", | |||||
| "react": "^18", | "react": "^18", | ||||
| "react-dom": "^18" | |||||
| "react-dom": "^18", | |||||
| "react-hook-form": "^7.49.2" | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@types/node": "^20", | "@types/node": "^20", | ||||
| @@ -285,10 +288,15 @@ | |||||
| "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | "node": "^12.22.0 || ^14.17.0 || >=16.0.0" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/@fontsource/roboto": { | |||||
| "version": "5.0.8", | |||||
| "resolved": "https://registry.npmjs.org/@fontsource/roboto/-/roboto-5.0.8.tgz", | |||||
| "integrity": "sha512-XxPltXs5R31D6UZeLIV1td3wTXU3jzd3f2DLsXI8tytMGBkIsGcc9sIyiupRtA8y73HAhuSCeweOoBqf6DbWCA==" | |||||
| "node_modules/@fontsource/inter": { | |||||
| "version": "5.0.16", | |||||
| "resolved": "https://registry.npmjs.org/@fontsource/inter/-/inter-5.0.16.tgz", | |||||
| "integrity": "sha512-qF0aH5UiZvCmneX5orJbVRoc2VTyLTV3X/7laMp03Qt28L+B9tFlZODOGUL64wDWc69YVdi1LeJB0cIgd51lvw==" | |||||
| }, | |||||
| "node_modules/@fontsource/plus-jakarta-sans": { | |||||
| "version": "5.0.18", | |||||
| "resolved": "https://registry.npmjs.org/@fontsource/plus-jakarta-sans/-/plus-jakarta-sans-5.0.18.tgz", | |||||
| "integrity": "sha512-poMuIcQ8F7WGXF4mNUviDk49Ewdf0pU7wmCzWQNbWEtus+L46BSp+4OqbWy0LWJEmMLI9F5hUHaSo2maLJwrQw==" | |||||
| }, | }, | ||||
| "node_modules/@humanwhocodes/config-array": { | "node_modules/@humanwhocodes/config-array": { | ||||
| "version": "0.11.13", | "version": "0.11.13", | ||||
| @@ -662,6 +670,126 @@ | |||||
| "node": ">= 10" | "node": ">= 10" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/@next/swc-darwin-x64": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.0.4.tgz", | |||||
| "integrity": "sha512-IZQ3C7Bx0k2rYtrZZxKKiusMTM9WWcK5ajyhOZkYYTCc8xytmwSzR1skU7qLgVT/EY9xtXDG0WhY6fyujnI3rw==", | |||||
| "cpu": [ | |||||
| "x64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "darwin" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-linux-arm64-gnu": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.0.4.tgz", | |||||
| "integrity": "sha512-VwwZKrBQo/MGb1VOrxJ6LrKvbpo7UbROuyMRvQKTFKhNaXjUmKTu7wxVkIuCARAfiI8JpaWAnKR+D6tzpCcM4w==", | |||||
| "cpu": [ | |||||
| "arm64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "linux" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-linux-arm64-musl": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.0.4.tgz", | |||||
| "integrity": "sha512-8QftwPEW37XxXoAwsn+nXlodKWHfpMaSvt81W43Wh8dv0gkheD+30ezWMcFGHLI71KiWmHK5PSQbTQGUiidvLQ==", | |||||
| "cpu": [ | |||||
| "arm64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "linux" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-linux-x64-gnu": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.0.4.tgz", | |||||
| "integrity": "sha512-/s/Pme3VKfZAfISlYVq2hzFS8AcAIOTnoKupc/j4WlvF6GQ0VouS2Q2KEgPuO1eMBwakWPB1aYFIA4VNVh667A==", | |||||
| "cpu": [ | |||||
| "x64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "linux" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-linux-x64-musl": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.0.4.tgz", | |||||
| "integrity": "sha512-m8z/6Fyal4L9Bnlxde5g2Mfa1Z7dasMQyhEhskDATpqr+Y0mjOBZcXQ7G5U+vgL22cI4T7MfvgtrM2jdopqWaw==", | |||||
| "cpu": [ | |||||
| "x64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "linux" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-win32-arm64-msvc": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.0.4.tgz", | |||||
| "integrity": "sha512-7Wv4PRiWIAWbm5XrGz3D8HUkCVDMMz9igffZG4NB1p4u1KoItwx9qjATHz88kwCEal/HXmbShucaslXCQXUM5w==", | |||||
| "cpu": [ | |||||
| "arm64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "win32" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-win32-ia32-msvc": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.0.4.tgz", | |||||
| "integrity": "sha512-zLeNEAPULsl0phfGb4kdzF/cAVIfaC7hY+kt0/d+y9mzcZHsMS3hAS829WbJ31DkSlVKQeHEjZHIdhN+Pg7Gyg==", | |||||
| "cpu": [ | |||||
| "ia32" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "win32" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@next/swc-win32-x64-msvc": { | |||||
| "version": "14.0.4", | |||||
| "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.0.4.tgz", | |||||
| "integrity": "sha512-yEh2+R8qDlDCjxVpzOTEpBLQTEFAcP2A8fUFLaWNap9GitYKkKv1//y2S6XY6zsR4rCOPRpU7plYDR+az2n30A==", | |||||
| "cpu": [ | |||||
| "x64" | |||||
| ], | |||||
| "optional": true, | |||||
| "os": [ | |||||
| "win32" | |||||
| ], | |||||
| "engines": { | |||||
| "node": ">= 10" | |||||
| } | |||||
| }, | |||||
| "node_modules/@nodelib/fs.walk": { | "node_modules/@nodelib/fs.walk": { | ||||
| "version": "1.2.8", | "version": "1.2.8", | ||||
| "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", | "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", | ||||
| @@ -12,13 +12,16 @@ | |||||
| "@emotion/cache": "^11.11.0", | "@emotion/cache": "^11.11.0", | ||||
| "@emotion/react": "^11.11.1", | "@emotion/react": "^11.11.1", | ||||
| "@emotion/styled": "^11.11.0", | "@emotion/styled": "^11.11.0", | ||||
| "@fontsource/roboto": "^5.0.8", | |||||
| "@fontsource/inter": "^5.0.16", | |||||
| "@fontsource/plus-jakarta-sans": "^5.0.18", | |||||
| "@mui/icons-material": "^5.15.0", | "@mui/icons-material": "^5.15.0", | ||||
| "@mui/material": "^5.15.0", | "@mui/material": "^5.15.0", | ||||
| "@mui/material-nextjs": "^5.15.0", | "@mui/material-nextjs": "^5.15.0", | ||||
| "next": "14.0.4", | "next": "14.0.4", | ||||
| "next-auth": "^4.24.5", | |||||
| "react": "^18", | "react": "^18", | ||||
| "react-dom": "^18" | |||||
| "react-dom": "^18", | |||||
| "react-hook-form": "^7.49.2" | |||||
| }, | }, | ||||
| "devDependencies": { | "devDependencies": { | ||||
| "@types/node": "^20", | "@types/node": "^20", | ||||
| @@ -0,0 +1,6 @@ | |||||
| import { authOptions } from "@/config/authConfig"; | |||||
| import NextAuth from "next-auth"; | |||||
| const handler = NextAuth(authOptions); | |||||
| export { handler as GET, handler as POST }; | |||||
| @@ -0,0 +1,5 @@ | |||||
| const Dashboard: React.FC = () => { | |||||
| return "Dashboard (protected)"; | |||||
| }; | |||||
| export default Dashboard; | |||||
| @@ -1,11 +1,6 @@ | |||||
| import type { Metadata } from "next"; | import type { Metadata } from "next"; | ||||
| import { AppRouterCacheProvider } from "@mui/material-nextjs/v14-appRouter"; | |||||
| import { CssBaseline } from "@mui/material"; | |||||
| import "@fontsource/roboto/300.css"; | |||||
| import "@fontsource/roboto/400.css"; | |||||
| import "@fontsource/roboto/500.css"; | |||||
| import "@fontsource/roboto/700.css"; | |||||
| import ThemeRegistry from "@/theme/ThemeRegistry"; | |||||
| export const metadata: Metadata = { | export const metadata: Metadata = { | ||||
| title: "Create Next App", | title: "Create Next App", | ||||
| @@ -20,8 +15,7 @@ export default function RootLayout({ | |||||
| return ( | return ( | ||||
| <html lang="en"> | <html lang="en"> | ||||
| <body> | <body> | ||||
| <CssBaseline enableColorScheme /> | |||||
| <AppRouterCacheProvider>{children}</AppRouterCacheProvider> | |||||
| <ThemeRegistry>{children}</ThemeRegistry> | |||||
| </body> | </body> | ||||
| </html> | </html> | ||||
| ); | ); | ||||
| @@ -0,0 +1,94 @@ | |||||
| "use client"; | |||||
| import { FormHelperText } from "@mui/material"; | |||||
| import Button from "@mui/material/Button"; | |||||
| import Stack from "@mui/material/Stack"; | |||||
| import TextField from "@mui/material/TextField"; | |||||
| import Typography from "@mui/material/Typography"; | |||||
| import { signIn } from "next-auth/react"; | |||||
| import { useRouter } from "next/navigation"; | |||||
| import { useState } from "react"; | |||||
| import { SubmitHandler, useForm } from "react-hook-form"; | |||||
| type LoginFields = { | |||||
| username: string; | |||||
| password: string; | |||||
| }; | |||||
| // Error codes in https://next-auth.js.org/configuration/pages#sign-in-page | |||||
| const getHumanFriendlyErrorMessage = (serverError: string): string => { | |||||
| switch (serverError) { | |||||
| case "CredentialsSignin": | |||||
| return "Invalid username or password."; | |||||
| case "Default": | |||||
| default: | |||||
| return "Something went wrong. Please try again later."; | |||||
| } | |||||
| }; | |||||
| const LoginForm: React.FC = () => { | |||||
| const { | |||||
| register, | |||||
| handleSubmit, | |||||
| formState: { errors, isSubmitting }, | |||||
| } = useForm<LoginFields>(); | |||||
| const [serverError, setServerError] = useState<string>(); | |||||
| const router = useRouter(); | |||||
| const onSubmit: SubmitHandler<LoginFields> = async (data) => { | |||||
| const res = await signIn("credentials", { | |||||
| redirect: false, | |||||
| ...data, | |||||
| }); | |||||
| if (res?.error) { | |||||
| setServerError(res.error); | |||||
| return; | |||||
| } | |||||
| const callbackUrl = | |||||
| new URLSearchParams(window.location.search).get("callbackUrl") || "/"; | |||||
| router.push(callbackUrl); | |||||
| }; | |||||
| return ( | |||||
| <Stack | |||||
| spacing={3} | |||||
| margin={5} | |||||
| component="form" | |||||
| onSubmit={handleSubmit(onSubmit)} | |||||
| > | |||||
| <Typography variant="h1">Sign In</Typography> | |||||
| <TextField | |||||
| label="Username" | |||||
| {...register("username", { required: "Please enter a username" })} | |||||
| error={Boolean(errors.username)} | |||||
| helperText={errors.username?.message} | |||||
| /> | |||||
| <TextField | |||||
| label="Password" | |||||
| type="password" | |||||
| {...register("password", { required: "Please enter a password" })} | |||||
| error={Boolean(errors.password)} | |||||
| helperText={errors.password?.message} | |||||
| /> | |||||
| {serverError && ( | |||||
| <FormHelperText error> | |||||
| {getHumanFriendlyErrorMessage(serverError)} | |||||
| </FormHelperText> | |||||
| )} | |||||
| <Button | |||||
| variant="contained" | |||||
| size="large" | |||||
| type="submit" | |||||
| disabled={isSubmitting} | |||||
| > | |||||
| Login | |||||
| </Button> | |||||
| </Stack> | |||||
| ); | |||||
| }; | |||||
| export default LoginForm; | |||||
| @@ -0,0 +1,28 @@ | |||||
| import Grid from "@mui/material/Grid"; | |||||
| import Paper from "@mui/material/Paper"; | |||||
| import LoginForm from "./LoginForm"; | |||||
| import { getServerSession } from "next-auth"; | |||||
| import { redirect } from "next/navigation"; | |||||
| import { authOptions } from "@/config/authConfig"; | |||||
| const Login: React.FC = async () => { | |||||
| const session = await getServerSession(authOptions); | |||||
| if (session?.user) { | |||||
| redirect("/"); | |||||
| } | |||||
| return ( | |||||
| <Grid container height="100vh"> | |||||
| <Grid item sm> | |||||
| Hero | |||||
| </Grid> | |||||
| <Grid item xs={12} sm={8} lg={5}> | |||||
| <Paper square sx={{ height: "100%" }}> | |||||
| <LoginForm /> | |||||
| </Paper> | |||||
| </Grid> | |||||
| </Grid> | |||||
| ); | |||||
| }; | |||||
| export default Login; | |||||
| @@ -1,5 +1,7 @@ | |||||
| const Home: React.FC = () => { | |||||
| return "home page"; | |||||
| import { permanentRedirect } from "next/navigation"; | |||||
| const Home: React.FC = async () => { | |||||
| permanentRedirect("/dashboard"); | |||||
| }; | }; | ||||
| export default Home; | export default Home; | ||||
| @@ -0,0 +1,2 @@ | |||||
| export const BASE_API_URL = `${process.env.API_PROTOCOL}://${process.env.API_HOST}:${process.env.API_PORT}`; | |||||
| export const LOGIN_API_PATH = `${BASE_API_URL}/api/login`; | |||||
| @@ -0,0 +1,42 @@ | |||||
| import { AuthOptions } from "next-auth"; | |||||
| import CredentialsProvider from "next-auth/providers/credentials"; | |||||
| import { LOGIN_API_PATH } from "./api"; | |||||
| export const authOptions: AuthOptions = { | |||||
| debug: process.env.NODE_ENV === "development", | |||||
| providers: [ | |||||
| CredentialsProvider({ | |||||
| id: "credentials", | |||||
| name: "Credentials", | |||||
| credentials: { | |||||
| username: { label: "Username", type: "text" }, | |||||
| password: { label: "Password", type: "password" }, | |||||
| }, | |||||
| async authorize(credentials, req) { | |||||
| const res = await fetch(LOGIN_API_PATH, { | |||||
| method: "POST", | |||||
| body: JSON.stringify(credentials), | |||||
| headers: { "Content-Type": "application/json" }, | |||||
| }); | |||||
| const user = await res.json(); | |||||
| if (res.ok && user) { | |||||
| return user; | |||||
| } | |||||
| return null; | |||||
| }, | |||||
| }), | |||||
| ], | |||||
| pages: { | |||||
| signIn: "/login", | |||||
| }, | |||||
| callbacks: { | |||||
| jwt(params) { | |||||
| return params.token; | |||||
| }, | |||||
| session(params) { | |||||
| return params.session; | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| @@ -0,0 +1,8 @@ | |||||
| import { withAuth } from "next-auth/middleware"; | |||||
| import { authOptions } from "@/config/authConfig"; | |||||
| export default withAuth({ | |||||
| pages: authOptions.pages, | |||||
| }); | |||||
| export const config = { matcher: ["/dashboard"] }; | |||||
| @@ -0,0 +1,100 @@ | |||||
| "use client"; | |||||
| import * as React from "react"; | |||||
| import createCache from "@emotion/cache"; | |||||
| import { useServerInsertedHTML } from "next/navigation"; | |||||
| import { CacheProvider as DefaultCacheProvider } from "@emotion/react"; | |||||
| import type { | |||||
| EmotionCache, | |||||
| Options as OptionsOfCreateCache, | |||||
| } from "@emotion/cache"; | |||||
| // Copied from https://github.com/mui/material-ui/blob/master/examples/material-ui-nextjs-ts/src/components/ThemeRegistry/EmotionCache.tsx | |||||
| export type NextAppDirEmotionCacheProviderProps = { | |||||
| /** This is the options passed to createCache() from 'import createCache from "@emotion/cache"' */ | |||||
| options: Omit<OptionsOfCreateCache, "insertionPoint">; | |||||
| /** By default <CacheProvider /> from 'import { CacheProvider } from "@emotion/react"' */ | |||||
| CacheProvider?: (props: { | |||||
| value: EmotionCache; | |||||
| children: React.ReactNode; | |||||
| }) => React.JSX.Element | null; | |||||
| children: React.ReactNode; | |||||
| }; | |||||
| // Adapted from https://github.com/garronej/tss-react/blob/main/src/next/appDir.tsx | |||||
| export default function NextAppDirEmotionCacheProvider( | |||||
| props: NextAppDirEmotionCacheProviderProps, | |||||
| ) { | |||||
| const { options, CacheProvider = DefaultCacheProvider, children } = props; | |||||
| const [registry] = React.useState(() => { | |||||
| const cache = createCache(options); | |||||
| cache.compat = true; | |||||
| const prevInsert = cache.insert; | |||||
| let inserted: { name: string; isGlobal: boolean }[] = []; | |||||
| cache.insert = (...args) => { | |||||
| const [selector, serialized] = args; | |||||
| if (cache.inserted[serialized.name] === undefined) { | |||||
| inserted.push({ | |||||
| name: serialized.name, | |||||
| isGlobal: !selector, | |||||
| }); | |||||
| } | |||||
| return prevInsert(...args); | |||||
| }; | |||||
| const flush = () => { | |||||
| const prevInserted = inserted; | |||||
| inserted = []; | |||||
| return prevInserted; | |||||
| }; | |||||
| return { cache, flush }; | |||||
| }); | |||||
| useServerInsertedHTML(() => { | |||||
| const inserted = registry.flush(); | |||||
| if (inserted.length === 0) { | |||||
| return null; | |||||
| } | |||||
| let styles = ""; | |||||
| let dataEmotionAttribute = registry.cache.key; | |||||
| const globals: { | |||||
| name: string; | |||||
| style: string; | |||||
| }[] = []; | |||||
| inserted.forEach(({ name, isGlobal }) => { | |||||
| const style = registry.cache.inserted[name]; | |||||
| if (typeof style !== "boolean") { | |||||
| if (isGlobal) { | |||||
| globals.push({ name, style }); | |||||
| } else { | |||||
| styles += style; | |||||
| dataEmotionAttribute += ` ${name}`; | |||||
| } | |||||
| } | |||||
| }); | |||||
| return ( | |||||
| <React.Fragment> | |||||
| {globals.map(({ name, style }) => ( | |||||
| <style | |||||
| key={name} | |||||
| data-emotion={`${registry.cache.key}-global ${name}`} | |||||
| // eslint-disable-next-line react/no-danger | |||||
| dangerouslySetInnerHTML={{ __html: style }} | |||||
| /> | |||||
| ))} | |||||
| {styles && ( | |||||
| <style | |||||
| data-emotion={dataEmotionAttribute} | |||||
| // eslint-disable-next-line react/no-danger | |||||
| dangerouslySetInnerHTML={{ __html: styles }} | |||||
| /> | |||||
| )} | |||||
| </React.Fragment> | |||||
| ); | |||||
| }); | |||||
| return <CacheProvider value={registry.cache}>{children}</CacheProvider>; | |||||
| } | |||||
| @@ -0,0 +1,22 @@ | |||||
| "use client"; | |||||
| import * as React from "react"; | |||||
| import { ThemeProvider } from "@mui/material/styles"; | |||||
| import CssBaseline from "@mui/material/CssBaseline"; | |||||
| import NextAppDirEmotionCacheProvider from "./EmotionCache"; | |||||
| import theme from "./devias-material-kit"; | |||||
| // Copied from https://github.com/mui/material-ui/blob/master/examples/material-ui-nextjs-ts/src/components/ThemeRegistry/ThemeRegistry.tsx | |||||
| export default function ThemeRegistry({ | |||||
| children, | |||||
| }: { | |||||
| children: React.ReactNode; | |||||
| }) { | |||||
| return ( | |||||
| <NextAppDirEmotionCacheProvider options={{ key: "mui" }}> | |||||
| <ThemeProvider theme={theme}> | |||||
| <CssBaseline /> | |||||
| {children} | |||||
| </ThemeProvider> | |||||
| </NextAppDirEmotionCacheProvider> | |||||
| ); | |||||
| } | |||||
| @@ -0,0 +1,57 @@ | |||||
| export const neutral = { | |||||
| 50: "#F8F9FA", | |||||
| 100: "#F3F4F6", | |||||
| 200: "#E5E7EB", | |||||
| 300: "#D2D6DB", | |||||
| 400: "#9DA4AE", | |||||
| 500: "#6C737F", | |||||
| 600: "#4D5761", | |||||
| 700: "#2F3746", | |||||
| 800: "#1C2536", | |||||
| 900: "#111927", | |||||
| }; | |||||
| export const indigo = { | |||||
| lightest: "#F5F7FF", | |||||
| light: "#EBEEFE", | |||||
| main: "#6366F1", | |||||
| dark: "#4338CA", | |||||
| darkest: "#312E81", | |||||
| contrastText: "#FFFFFF", | |||||
| }; | |||||
| export const success = { | |||||
| lightest: "#F0FDF9", | |||||
| light: "#3FC79A", | |||||
| main: "#10B981", | |||||
| dark: "#0B815A", | |||||
| darkest: "#134E48", | |||||
| contrastText: "#FFFFFF", | |||||
| }; | |||||
| export const info = { | |||||
| lightest: "#ECFDFF", | |||||
| light: "#CFF9FE", | |||||
| main: "#06AED4", | |||||
| dark: "#0E7090", | |||||
| darkest: "#164C63", | |||||
| contrastText: "#FFFFFF", | |||||
| }; | |||||
| export const warning = { | |||||
| lightest: "#FFFAEB", | |||||
| light: "#FEF0C7", | |||||
| main: "#F79009", | |||||
| dark: "#B54708", | |||||
| darkest: "#7A2E0E", | |||||
| contrastText: "#FFFFFF", | |||||
| }; | |||||
| export const error = { | |||||
| lightest: "#FEF3F2", | |||||
| light: "#FEE4E2", | |||||
| main: "#F04438", | |||||
| dark: "#B42318", | |||||
| darkest: "#7A271A", | |||||
| contrastText: "#FFFFFF", | |||||
| }; | |||||
| @@ -0,0 +1,288 @@ | |||||
| import { ThemeOptions, createTheme } from "@mui/material"; | |||||
| import palette from "./palette"; | |||||
| // Used only to create transitions | |||||
| const muiTheme = createTheme(); | |||||
| const components: ThemeOptions["components"] = { | |||||
| MuiAvatar: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| fontSize: 14, | |||||
| fontWeight: 600, | |||||
| letterSpacing: 0, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiButton: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| borderRadius: "12px", | |||||
| textTransform: "none", | |||||
| }, | |||||
| sizeSmall: { | |||||
| padding: "6px 16px", | |||||
| }, | |||||
| sizeMedium: { | |||||
| padding: "8px 20px", | |||||
| }, | |||||
| sizeLarge: { | |||||
| padding: "11px 24px", | |||||
| }, | |||||
| textSizeSmall: { | |||||
| padding: "7px 12px", | |||||
| }, | |||||
| textSizeMedium: { | |||||
| padding: "9px 16px", | |||||
| }, | |||||
| textSizeLarge: { | |||||
| padding: "12px 16px", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiCard: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| borderRadius: 20, | |||||
| [`&.MuiPaper-elevation1`]: { | |||||
| boxShadow: | |||||
| "0px 5px 22px rgba(0, 0, 0, 0.04), 0px 0px 0px 0.5px rgba(0, 0, 0, 0.03)", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiCardContent: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| padding: "32px 24px", | |||||
| "&:last-child": { | |||||
| paddingBottom: "32px", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiCardHeader: { | |||||
| defaultProps: { | |||||
| titleTypographyProps: { | |||||
| variant: "h6", | |||||
| }, | |||||
| subheaderTypographyProps: { | |||||
| variant: "body2", | |||||
| }, | |||||
| }, | |||||
| styleOverrides: { | |||||
| root: { | |||||
| padding: "32px 24px 16px", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiCssBaseline: { | |||||
| styleOverrides: { | |||||
| "*": { | |||||
| boxSizing: "border-box", | |||||
| }, | |||||
| html: { | |||||
| MozOsxFontSmoothing: "grayscale", | |||||
| WebkitFontSmoothing: "antialiased", | |||||
| display: "flex", | |||||
| flexDirection: "column", | |||||
| minHeight: "100%", | |||||
| width: "100%", | |||||
| }, | |||||
| body: { | |||||
| display: "flex", | |||||
| flex: "1 1 auto", | |||||
| flexDirection: "column", | |||||
| minHeight: "100%", | |||||
| width: "100%", | |||||
| }, | |||||
| "#__next": { | |||||
| display: "flex", | |||||
| flex: "1 1 auto", | |||||
| flexDirection: "column", | |||||
| height: "100%", | |||||
| width: "100%", | |||||
| }, | |||||
| "#nprogress": { | |||||
| pointerEvents: "none", | |||||
| }, | |||||
| "#nprogress .bar": { | |||||
| backgroundColor: palette.primary.main, | |||||
| height: 3, | |||||
| left: 0, | |||||
| position: "fixed", | |||||
| top: 0, | |||||
| width: "100%", | |||||
| zIndex: 2000, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiInputBase: { | |||||
| styleOverrides: { | |||||
| input: { | |||||
| "&::placeholder": { | |||||
| opacity: 1, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiInput: { | |||||
| styleOverrides: { | |||||
| input: { | |||||
| fontSize: 14, | |||||
| fontWeight: 500, | |||||
| lineHeight: "24px", | |||||
| "&::placeholder": { | |||||
| color: palette.text.secondary, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiFilledInput: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| backgroundColor: "transparent", | |||||
| borderRadius: 8, | |||||
| borderStyle: "solid", | |||||
| borderWidth: 1, | |||||
| overflow: "hidden", | |||||
| borderColor: palette.neutral[200], | |||||
| transition: muiTheme.transitions.create(["border-color", "box-shadow"]), | |||||
| "&:hover": { | |||||
| backgroundColor: palette.action.hover, | |||||
| }, | |||||
| "&:before": { | |||||
| display: "none", | |||||
| }, | |||||
| "&:after": { | |||||
| display: "none", | |||||
| }, | |||||
| [`&.Mui-disabled`]: { | |||||
| backgroundColor: "transparent", | |||||
| }, | |||||
| [`&.Mui-focused`]: { | |||||
| backgroundColor: "transparent", | |||||
| borderColor: palette.primary.main, | |||||
| boxShadow: `${palette.primary.main} 0 0 0 2px`, | |||||
| }, | |||||
| [`&.Mui-error`]: { | |||||
| borderColor: palette.error.main, | |||||
| boxShadow: `${palette.error.main} 0 0 0 2px`, | |||||
| }, | |||||
| }, | |||||
| input: { | |||||
| fontSize: 14, | |||||
| fontWeight: 500, | |||||
| lineHeight: "24px", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiOutlinedInput: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| "&:hover": { | |||||
| backgroundColor: palette.action.hover, | |||||
| [`& .MuiOutlinedInput-notchedOutline`]: { | |||||
| borderColor: palette.neutral[200], | |||||
| }, | |||||
| }, | |||||
| [`&.Mui-focused`]: { | |||||
| backgroundColor: "transparent", | |||||
| [`& .MuiOutlinedInput-notchedOutline`]: { | |||||
| borderColor: palette.primary.main, | |||||
| boxShadow: `${palette.primary.main} 0 0 0 2px`, | |||||
| }, | |||||
| }, | |||||
| [`&.Mui-error`]: { | |||||
| [`& .MuiOutlinedInput-notchedOutline`]: { | |||||
| borderColor: palette.error.main, | |||||
| boxShadow: `${palette.error.main} 0 0 0 2px`, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| input: { | |||||
| fontSize: 14, | |||||
| fontWeight: 500, | |||||
| lineHeight: "24px", | |||||
| }, | |||||
| notchedOutline: { | |||||
| borderColor: palette.neutral[200], | |||||
| transition: muiTheme.transitions.create(["border-color", "box-shadow"]), | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiFormLabel: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| fontSize: 14, | |||||
| fontWeight: 500, | |||||
| [`&.MuiInputLabel-filled`]: { | |||||
| transform: "translate(12px, 18px) scale(1)", | |||||
| }, | |||||
| [`&.MuiInputLabel-shrink`]: { | |||||
| [`&.MuiInputLabel-standard`]: { | |||||
| transform: "translate(0, -1.5px) scale(0.85)", | |||||
| }, | |||||
| [`&.MuiInputLabel-filled`]: { | |||||
| transform: "translate(12px, 6px) scale(0.85)", | |||||
| }, | |||||
| [`&.MuiInputLabel-outlined`]: { | |||||
| transform: "translate(14px, -9px) scale(0.85)", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiTab: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| fontSize: 14, | |||||
| fontWeight: 500, | |||||
| lineHeight: 1.71, | |||||
| minWidth: "auto", | |||||
| paddingLeft: 0, | |||||
| paddingRight: 0, | |||||
| textTransform: "none", | |||||
| "& + &": { | |||||
| marginLeft: 24, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiTableCell: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| borderBottomColor: palette.divider, | |||||
| padding: "15px 16px", | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiTableHead: { | |||||
| styleOverrides: { | |||||
| root: { | |||||
| borderBottom: "none", | |||||
| [`& .MuiTableCell-root`]: { | |||||
| borderBottom: "none", | |||||
| backgroundColor: palette.neutral[50], | |||||
| color: palette.neutral[700], | |||||
| fontSize: 12, | |||||
| fontWeight: 600, | |||||
| lineHeight: 1, | |||||
| letterSpacing: 0.5, | |||||
| textTransform: "uppercase", | |||||
| }, | |||||
| [`& .MuiTableCell-paddingCheckbox`]: { | |||||
| paddingTop: 4, | |||||
| paddingBottom: 4, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| }, | |||||
| MuiTextField: { | |||||
| defaultProps: { | |||||
| variant: "filled", | |||||
| }, | |||||
| }, | |||||
| }; | |||||
| export default components; | |||||
| @@ -0,0 +1,26 @@ | |||||
| import { createTheme } from "@mui/material"; | |||||
| import { paletteOptions as palette } from "./palette"; | |||||
| import components from "./components"; | |||||
| import shadows from "./shadows"; | |||||
| import typography from "./typography"; | |||||
| const theme = createTheme({ | |||||
| breakpoints: { | |||||
| values: { | |||||
| xs: 0, | |||||
| sm: 600, | |||||
| md: 900, | |||||
| lg: 1200, | |||||
| xl: 1440, | |||||
| }, | |||||
| }, | |||||
| components, | |||||
| palette, | |||||
| shadows, | |||||
| shape: { | |||||
| borderRadius: 8, | |||||
| }, | |||||
| typography, | |||||
| }); | |||||
| export default theme; | |||||
| @@ -0,0 +1,35 @@ | |||||
| import { common } from "@mui/material/colors"; | |||||
| import { PaletteOptions } from "@mui/material/styles"; | |||||
| import { error, indigo, info, neutral, success, warning } from "./colors"; | |||||
| const palette = { | |||||
| action: { | |||||
| active: neutral[500], | |||||
| disabled: "rgba(17,25,39,0.38)", | |||||
| disabledBackground: "rgba(17,25,39,0.12)", | |||||
| focus: "rgba(17,25,39,0.16)", | |||||
| hover: "rgba(17,25,39,0.04)", | |||||
| selected: "rgba(17,25,39,0.12)", | |||||
| }, | |||||
| background: { | |||||
| default: common.white, | |||||
| paper: common.white, | |||||
| }, | |||||
| divider: "#F2F4F7", | |||||
| error, | |||||
| info, | |||||
| mode: "light", | |||||
| primary: indigo, | |||||
| success, | |||||
| text: { | |||||
| primary: neutral[900], | |||||
| secondary: neutral[500], | |||||
| disabled: "rgba(17,25,39,0.38)", | |||||
| }, | |||||
| warning, | |||||
| neutral, | |||||
| }; | |||||
| export const paletteOptions: PaletteOptions = { ...palette, mode: "light" }; | |||||
| export default palette; | |||||
| @@ -0,0 +1,31 @@ | |||||
| import { Shadows } from "@mui/material"; | |||||
| const shadows: Shadows = [ | |||||
| "none", | |||||
| "0px 1px 2px rgba(0, 0, 0, 0.08)", | |||||
| "0px 1px 5px rgba(0, 0, 0, 0.08)", | |||||
| "0px 1px 8px rgba(0, 0, 0, 0.08)", | |||||
| "0px 1px 10px rgba(0, 0, 0, 0.08)", | |||||
| "0px 1px 14px rgba(0, 0, 0, 0.08)", | |||||
| "0px 1px 18px rgba(0, 0, 0, 0.08)", | |||||
| "0px 2px 16px rgba(0, 0, 0, 0.08)", | |||||
| "0px 3px 14px rgba(0, 0, 0, 0.08)", | |||||
| "0px 3px 16px rgba(0, 0, 0, 0.08)", | |||||
| "0px 4px 18px rgba(0, 0, 0, 0.08)", | |||||
| "0px 4px 20px rgba(0, 0, 0, 0.08)", | |||||
| "0px 5px 22px rgba(0, 0, 0, 0.08)", | |||||
| "0px 5px 24px rgba(0, 0, 0, 0.08)", | |||||
| "0px 5px 26px rgba(0, 0, 0, 0.08)", | |||||
| "0px 6px 28px rgba(0, 0, 0, 0.08)", | |||||
| "0px 6px 30px rgba(0, 0, 0, 0.08)", | |||||
| "0px 6px 32px rgba(0, 0, 0, 0.08)", | |||||
| "0px 7px 34px rgba(0, 0, 0, 0.08)", | |||||
| "0px 7px 36px rgba(0, 0, 0, 0.08)", | |||||
| "0px 8px 38px rgba(0, 0, 0, 0.08)", | |||||
| "0px 8px 40px rgba(0, 0, 0, 0.08)", | |||||
| "0px 8px 42px rgba(0, 0, 0, 0.08)", | |||||
| "0px 9px 44px rgba(0, 0, 0, 0.08)", | |||||
| "0px 9px 46px rgba(0, 0, 0, 0.08)", | |||||
| ]; | |||||
| export default shadows; | |||||
| @@ -0,0 +1,90 @@ | |||||
| import { TypographyOptions } from "@mui/material/styles/createTypography"; | |||||
| import "@fontsource/inter/400.css"; | |||||
| import "@fontsource/inter/500.css"; | |||||
| import "@fontsource/inter/600.css"; | |||||
| import "@fontsource/plus-jakarta-sans/700.css"; | |||||
| const typography: TypographyOptions = { | |||||
| fontFamily: | |||||
| '"Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji"', | |||||
| body1: { | |||||
| fontSize: "1rem", | |||||
| fontWeight: 400, | |||||
| lineHeight: 1.5, | |||||
| }, | |||||
| body2: { | |||||
| fontSize: "0.875rem", | |||||
| fontWeight: 400, | |||||
| lineHeight: 1.57, | |||||
| }, | |||||
| button: { | |||||
| fontWeight: 600, | |||||
| }, | |||||
| caption: { | |||||
| fontSize: "0.75rem", | |||||
| fontWeight: 500, | |||||
| lineHeight: 1.66, | |||||
| }, | |||||
| subtitle1: { | |||||
| fontSize: "1rem", | |||||
| fontWeight: 500, | |||||
| lineHeight: 1.57, | |||||
| }, | |||||
| subtitle2: { | |||||
| fontSize: "0.875rem", | |||||
| fontWeight: 500, | |||||
| lineHeight: 1.57, | |||||
| }, | |||||
| overline: { | |||||
| fontSize: "0.75rem", | |||||
| fontWeight: 600, | |||||
| letterSpacing: "0.5px", | |||||
| lineHeight: 2.5, | |||||
| textTransform: "uppercase", | |||||
| }, | |||||
| h1: { | |||||
| fontFamily: "'Plus Jakarta Sans', sans-serif", | |||||
| fontWeight: 700, | |||||
| fontSize: "3.5rem", | |||||
| lineHeight: 1.2, | |||||
| }, | |||||
| h2: { | |||||
| fontFamily: "'Plus Jakarta Sans', sans-serif", | |||||
| fontWeight: 700, | |||||
| fontSize: "3rem", | |||||
| lineHeight: 1.2, | |||||
| }, | |||||
| h3: { | |||||
| fontFamily: "'Plus Jakarta Sans', sans-serif", | |||||
| fontWeight: 700, | |||||
| fontSize: "2.25rem", | |||||
| lineHeight: 1.2, | |||||
| }, | |||||
| h4: { | |||||
| fontFamily: "'Plus Jakarta Sans', sans-serif", | |||||
| fontWeight: 700, | |||||
| fontSize: "2rem", | |||||
| lineHeight: 1.2, | |||||
| }, | |||||
| h5: { | |||||
| fontFamily: "'Plus Jakarta Sans', sans-serif", | |||||
| fontWeight: 700, | |||||
| fontSize: "1.5rem", | |||||
| lineHeight: 1.2, | |||||
| }, | |||||
| h6: { | |||||
| fontFamily: "'Plus Jakarta Sans', sans-serif", | |||||
| fontWeight: 700, | |||||
| fontSize: "1.125rem", | |||||
| lineHeight: 1.2, | |||||
| }, | |||||
| fontSize: 0, | |||||
| fontWeightLight: undefined, | |||||
| fontWeightRegular: undefined, | |||||
| fontWeightMedium: undefined, | |||||
| fontWeightBold: undefined, | |||||
| htmlFontSize: 0, | |||||
| }; | |||||
| export default typography; | |||||