@@ -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; |