| @@ -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/react": "^11.11.1", | |||
| "@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/material": "^5.15.0", | |||
| "@mui/material-nextjs": "^5.15.0", | |||
| "next": "14.0.4", | |||
| "next-auth": "^4.24.5", | |||
| "react": "^18", | |||
| "react-dom": "^18" | |||
| "react-dom": "^18", | |||
| "react-hook-form": "^7.49.2" | |||
| }, | |||
| "devDependencies": { | |||
| "@types/node": "^20", | |||
| @@ -285,10 +288,15 @@ | |||
| "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": { | |||
| "version": "0.11.13", | |||
| @@ -662,6 +670,126 @@ | |||
| "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": { | |||
| "version": "1.2.8", | |||
| "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", | |||
| @@ -12,13 +12,16 @@ | |||
| "@emotion/cache": "^11.11.0", | |||
| "@emotion/react": "^11.11.1", | |||
| "@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/material": "^5.15.0", | |||
| "@mui/material-nextjs": "^5.15.0", | |||
| "next": "14.0.4", | |||
| "next-auth": "^4.24.5", | |||
| "react": "^18", | |||
| "react-dom": "^18" | |||
| "react-dom": "^18", | |||
| "react-hook-form": "^7.49.2" | |||
| }, | |||
| "devDependencies": { | |||
| "@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 { 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 = { | |||
| title: "Create Next App", | |||
| @@ -20,8 +15,7 @@ export default function RootLayout({ | |||
| return ( | |||
| <html lang="en"> | |||
| <body> | |||
| <CssBaseline enableColorScheme /> | |||
| <AppRouterCacheProvider>{children}</AppRouterCacheProvider> | |||
| <ThemeRegistry>{children}</ThemeRegistry> | |||
| </body> | |||
| </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; | |||
| @@ -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; | |||