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