Compare commits

..

7 Commits

37 changed files with 355 additions and 691 deletions

7
.gitignore vendored
View File

@@ -39,3 +39,10 @@ yarn-error.log*
# typescript # typescript
*.tsbuildinfo *.tsbuildinfo
next-env.d.ts next-env.d.ts
# repomix
repomix-output.txt
# vscode
.vscode/*
!.vscode/settings.json

3
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,3 @@
{
"scss.lint.unknownAtRules": "ignore"
}

BIN
bun.lockb

Binary file not shown.

View File

@@ -1,9 +1,14 @@
import type { NextConfig } from "next"; import type { NextConfig } from "next";
import path from "node:path";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: "standalone", output: "standalone",
experimental: { experimental: {
optimizePackageImports: ["@chakra-ui/react"], optimizePackageImports: ["@mantine/core", "@mantine/hooks"],
},
sassOptions: {
implementation: "sass-embedded",
additionalData: `@use "${path.join(process.cwd(), "src/_mantine").replace(/\\/g, "/")}" as mantine;`,
}, },
}; };

View File

@@ -9,13 +9,16 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@chakra-ui/react": "^3.4.0", "@mantine/core": "^7.16.1",
"@emotion/react": "^11.14.0", "@mantine/form": "^7.16.1",
"@mantine/hooks": "^7.16.1",
"@mantine/modals": "^7.16.1",
"next": "15.1.5", "next": "15.1.5",
"next-themes": "^0.4.4", "next-auth": "^4.24.11",
"react": "^19.0.0", "react": "^19.0.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-icons": "^5.4.0" "react-icons": "^5.4.0",
"zod": "^3.24.1"
}, },
"devDependencies": { "devDependencies": {
"@eslint/eslintrc": "^3", "@eslint/eslintrc": "^3",
@@ -24,8 +27,10 @@
"@types/react-dom": "^19", "@types/react-dom": "^19",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "15.1.5", "eslint-config-next": "15.1.5",
"postcss": "^8", "postcss": "^8.5.1",
"sass": "^1.83.4", "postcss-preset-mantine": "^1.17.0",
"postcss-simple-vars": "^7.0.1",
"sass-embedded": "^1.83.4",
"typescript": "^5" "typescript": "^5"
} }
} }

14
postcss.config.cjs Normal file
View File

@@ -0,0 +1,14 @@
module.exports = {
plugins: {
"postcss-preset-mantine": {},
"postcss-simple-vars": {
variables: {
"mantine-breakpoint-xs": "36em",
"mantine-breakpoint-sm": "48em",
"mantine-breakpoint-md": "62em",
"mantine-breakpoint-lg": "75em",
"mantine-breakpoint-xl": "88em",
},
},
},
};

View File

@@ -1,6 +0,0 @@
/** @type {import('postcss-load-config').Config} */
const config = {
plugins: {},
};
export default config;

57
src/_mantine.scss Normal file
View File

@@ -0,0 +1,57 @@
@use 'sass:math';
// Define variables for your breakpoints,
// values must be the same as in your theme
$mantine-breakpoint-xs: '36em';
$mantine-breakpoint-sm: '48em';
$mantine-breakpoint-md: '62em';
$mantine-breakpoint-lg: '75em';
$mantine-breakpoint-xl: '88em';
@function rem($value) {
@return #{math.div(math.div($value, $value * 0 + 1), 16)}rem;
}
@mixin light {
[data-mantine-color-scheme='light'] & {
@content;
}
}
@mixin dark {
[data-mantine-color-scheme='dark'] & {
@content;
}
}
@mixin hover {
@media (hover: hover) {
&:hover {
@content;
}
}
@media (hover: none) {
&:active {
@content;
}
}
}
@mixin smaller-than($breakpoint) {
@media (max-width: $breakpoint) {
@content;
}
}
@mixin larger-than($breakpoint) {
@media (min-width: $breakpoint) {
@content;
}
}
@mixin ltr {
[dir='ltr'] & {
@content;
}
}

View File

View File

@@ -0,0 +1,83 @@
"use client";
import { Button, Card, Flex, PasswordInput, Stack, Text, TextInput } from "@mantine/core";
import { hasLength, useForm } from "@mantine/form";
import { signIn } from "next-auth/react";
import { useRouter } from "next/navigation";
interface FormValues {
name: string;
password: string;
}
export default function LoginPage() {
const router = useRouter();
const form = useForm<FormValues>({
mode: "uncontrolled",
initialValues: {
name: "",
password: "",
},
validate: {
name: hasLength({ min: 5 }, "Too short"),
password: hasLength({ min: 5 }, "Too short"),
},
});
const handleSubmit = async (formData: { name: string; password: string }) => {
const res = await signIn("credentials", {
username: formData.name,
password: formData.password,
redirect: false,
});
if (res && res.error) {
if (res.status === 401)
form.setErrors({
name: "Wrong password or username",
});
else
form.setErrors({
name: `Unknown error: ${res.status}`,
});
} else if (res && res.ok) {
router.push("/admin/panel");
} else {
form.setErrors({ name: "Unknown error" });
}
};
return (
<Flex w="100vw" h="100vh" justify="center" align="center">
<Card shadow="md" w="400" radius="md">
<form onSubmit={form.onSubmit(handleSubmit)}>
<Stack>
<Text c="dimmed" size="xl" ta="center">
Admin
</Text>
<TextInput
label="Name"
placeholder="Username"
key={form.key("name")}
withAsterisk
{...form.getInputProps("name")}
name="name"
autoComplete="username"
/>
<PasswordInput
label="Password"
mt="md"
withAsterisk
placeholder="password"
key={form.key("password")}
name="password"
autoComplete="current-password"
{...form.getInputProps("password")}
/>
<Button radius="md" type="submit" mt="md" w="100%">
Login
</Button>
</Stack>
</form>
</Card>
</Flex>
);
}

9
src/app/admin/page.tsx Normal file
View File

@@ -0,0 +1,9 @@
"use server";
import { redirect } from "next/navigation";
const AdminPage = () => {
redirect("/admin/panel");
};
export default AdminPage;

View File

@@ -0,0 +1,54 @@
"use client";
import { ActionIcon, AppShell, Burger, Button, Flex, Group, Skeleton, useMantineColorScheme } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { signOut } from "next-auth/react";
import { useRouter } from "next/navigation";
import { LuMoon, LuSun } from "react-icons/lu";
import classes from "./panel.module.scss";
const AdminPanelLayout = ({
children,
}: Readonly<{
children: React.ReactNode;
}>) => {
const [opened, { toggle }] = useDisclosure();
const { setColorScheme, colorScheme } = useMantineColorScheme();
const router = useRouter();
const changeColorScheme = () => {
setColorScheme(colorScheme === "light" ? "dark" : "light");
};
const handleSignOut = async () => {
const data = await signOut({ redirect: false, callbackUrl: "/" });
router.push(data.url);
};
return (
<AppShell header={{ height: 60 }} navbar={{ width: 300, breakpoint: "sm", collapsed: { mobile: !opened } }}>
<AppShell.Header>
<Flex h="100%" px="md" justify="space-between" align="center">
<Group h="100%">
<Burger opened={opened} onClick={toggle} size="sm" hiddenFrom="sm" />
<div>Logo</div>
</Group>
<ActionIcon onClick={changeColorScheme} variant="default" size="md" aria-label="Toggle color scheme">
{colorScheme === "light" ? <LuMoon /> : <LuSun />}
</ActionIcon>
</Flex>
</AppShell.Header>
<AppShell.Navbar p="md">
{Array(15)
.fill(0)
.map((_, index) => (
<Skeleton key={index} h={28} mt="sm" animate={false} />
))}
<Button h={28} mt="sm" onClick={handleSignOut}>
Sign Out
</Button>
</AppShell.Navbar>
<AppShell.Main>{children}</AppShell.Main>
<AppShell.Footer>
<div className={classes.footer}>Test</div>{" "}
</AppShell.Footer>
</AppShell>
);
};
export default AdminPanelLayout;

View File

@@ -0,0 +1,6 @@
"use client";
const PanelPage = () => {
return <div>Panel</div>;
};
export default PanelPage;

View File

@@ -0,0 +1,6 @@
@media(min-width: 48em) {
.footer {
margin-inline-start: var(--app-shell-navbar-width);
width: calc(100vw - var(--app-shell-navbar-width));
}
}

View File

@@ -0,0 +1,33 @@
import NextAuth from "next-auth";
import CredentialsProvider from "next-auth/providers/credentials";
const handler = NextAuth({
jwt: {
maxAge: 60 * 60 * 24 * 30,
},
session: {
strategy: "jwt",
},
pages: {
signIn: "/admin/login",
},
providers: [
CredentialsProvider({
name: "Credentials",
credentials: {
username: { label: "Username", type: "text", placeholder: "jsmith" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const { username, password } = credentials as { username: string; password: string };
if (username !== "admin" || password !== "admin") {
return null;
}
return new Promise((resolve) => resolve({ id: "1", email: "example@example.org", name: "test" }));
},
}),
],
});
export { handler as GET, handler as POST };

3
src/app/api/route.ts Normal file
View File

@@ -0,0 +1,3 @@
export async function GET() {
return Response.json({ test: 1 });
}

View File

@@ -1,8 +0,0 @@
export default async function Post({ params }: { params: Promise<{ slug: string }> }) {
const { slug } = await params;
return (
<div>
<h1>Post: {slug}</h1>
</div>
);
}

View File

@@ -1,3 +0,0 @@
export default function Blog() {
return <h1>Blog</h1>;
}

View File

@@ -1,4 +1,7 @@
import { ColorSchemeScript, MantineProvider } from "@mantine/core";
import "@mantine/core/styles.css";
import type { Metadata } from "next"; import type { Metadata } from "next";
import { getSession } from "next-auth/react";
import { Geist, Geist_Mono } from "next/font/google"; import { Geist, Geist_Mono } from "next/font/google";
import "./globals.scss"; import "./globals.scss";
import Providers from "./providers"; import Providers from "./providers";
@@ -18,15 +21,21 @@ export const metadata: Metadata = {
description: "Generated by create next app", description: "Generated by create next app",
}; };
export default function RootLayout({ export default async function RootLayout({
children, children,
}: Readonly<{ }: Readonly<{
children: React.ReactNode; children: React.ReactNode;
}>) { }>) {
const session = await getSession();
return ( return (
<html lang="ru" suppressHydrationWarning> <html lang="ru" suppressHydrationWarning>
<head>
<ColorSchemeScript />
</head>
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}> <body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
<Providers>{children}</Providers> <MantineProvider>
<Providers session={session}>{children}</Providers>
</MantineProvider>
</body> </body>
</html> </html>
); );

View File

@@ -1,18 +1,47 @@
"use client"; "use client";
import { Button } from "@/components/ui/button"; import { ActionIcon, AppShell, Burger, Flex, Group, Skeleton, useMantineColorScheme } from "@mantine/core";
import { useColorMode, useColorModeValue } from "@/components/ui/color-mode"; import { useDisclosure } from "@mantine/hooks";
import { VStack } from "@chakra-ui/react"; import { LuMoon, LuSun } from "react-icons/lu";
export default function Home() { export default function Home() {
const { toggleColorMode } = useColorMode(); const [opened, { toggle }] = useDisclosure();
const color = useColorModeValue("red", "blue"); const { setColorScheme, colorScheme } = useMantineColorScheme();
return ( return (
<VStack> <AppShell
<Button onClick={toggleColorMode}>Test 1</Button> header={{ height: 60 }}
<Button colorPalette={color} variant="solid"> navbar={{ width: 300, breakpoint: "sm", collapsed: { mobile: !opened } }}
{color} padding="md"
</Button> >
</VStack> <AppShell.Header>
<Flex h="100%" px="md" justify="space-between" align="center">
<Group h="100%" px="md">
<Burger opened={opened} onClick={toggle} size="sm" hiddenFrom="sm" />
<div>Logo</div>
</Group>
<ActionIcon
onClick={() => {
setColorScheme(colorScheme === "light" ? "dark" : "light");
}}
variant="default"
size="md"
aria-label="Toggle color scheme"
>
{colorScheme === "light" ? <LuMoon /> : <LuSun />}
</ActionIcon>
</Flex>
</AppShell.Header>
<AppShell.Navbar p="md">
{Array(15)
.fill(0)
.map((_, index) => (
<Skeleton key={index} h={28} mt="sm" animate={false} />
))}
</AppShell.Navbar>
<AppShell.Main>Main</AppShell.Main>
<AppShell.Footer>
<div>Footer</div>
</AppShell.Footer>
</AppShell>
); );
} }

View File

@@ -1,5 +1,6 @@
import { Provider } from "@/components/ui/provider"; "use client";
import type { Session } from "next-auth";
export default function Providers(props: React.PropsWithChildren) { import { SessionProvider } from "next-auth/react";
return <Provider>{props.children}</Provider>; export default function Providers({ session, children }: { session: Session | null; children: React.ReactNode }) {
return <SessionProvider session={session}>{children}</SessionProvider>;
} }

View File

@@ -1,74 +0,0 @@
"use client"
import type { GroupProps, SlotRecipeProps } from "@chakra-ui/react"
import { Avatar as ChakraAvatar, Group } from "@chakra-ui/react"
import * as React from "react"
type ImageProps = React.ImgHTMLAttributes<HTMLImageElement>
export interface AvatarProps extends ChakraAvatar.RootProps {
name?: string
src?: string
srcSet?: string
loading?: ImageProps["loading"]
icon?: React.ReactElement
fallback?: React.ReactNode
}
export const Avatar = React.forwardRef<HTMLDivElement, AvatarProps>(
function Avatar(props, ref) {
const { name, src, srcSet, loading, icon, fallback, children, ...rest } =
props
return (
<ChakraAvatar.Root ref={ref} {...rest}>
<AvatarFallback name={name} icon={icon}>
{fallback}
</AvatarFallback>
<ChakraAvatar.Image src={src} srcSet={srcSet} loading={loading} />
{children}
</ChakraAvatar.Root>
)
},
)
interface AvatarFallbackProps extends ChakraAvatar.FallbackProps {
name?: string
icon?: React.ReactElement
}
const AvatarFallback = React.forwardRef<HTMLDivElement, AvatarFallbackProps>(
function AvatarFallback(props, ref) {
const { name, icon, children, ...rest } = props
return (
<ChakraAvatar.Fallback ref={ref} {...rest}>
{children}
{name != null && children == null && <>{getInitials(name)}</>}
{name == null && children == null && (
<ChakraAvatar.Icon asChild={!!icon}>{icon}</ChakraAvatar.Icon>
)}
</ChakraAvatar.Fallback>
)
},
)
function getInitials(name: string) {
const names = name.trim().split(" ")
const firstName = names[0] != null ? names[0] : ""
const lastName = names.length > 1 ? names[names.length - 1] : ""
return firstName && lastName
? `${firstName.charAt(0)}${lastName.charAt(0)}`
: firstName.charAt(0)
}
interface AvatarGroupProps extends GroupProps, SlotRecipeProps<"avatar"> {}
export const AvatarGroup = React.forwardRef<HTMLDivElement, AvatarGroupProps>(
function AvatarGroup(props, ref) {
const { size, variant, borderless, ...rest } = props
return (
<ChakraAvatar.PropsProvider value={{ size, variant, borderless }}>
<Group gap="0" spaceX="-3" ref={ref} {...rest} />
</ChakraAvatar.PropsProvider>
)
},
)

View File

@@ -1,33 +0,0 @@
import type { ButtonProps as ChakraButtonProps } from "@chakra-ui/react";
import { AbsoluteCenter, Button as ChakraButton, Span, Spinner } from "@chakra-ui/react";
import * as React from "react";
interface ButtonLoadingProps {
loading?: boolean;
loadingText?: React.ReactNode;
}
export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {}
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(function Button(props, ref) {
const { loading, disabled, loadingText, children, ...rest } = props;
return (
<ChakraButton disabled={loading || disabled} ref={ref} {...rest}>
{loading && !loadingText ? (
<>
<AbsoluteCenter display="inline-flex">
<Spinner size="inherit" color="inherit" />
</AbsoluteCenter>
<Span opacity={0}>{children}</Span>
</>
) : loading && loadingText ? (
<>
<Spinner size="inherit" color="inherit" />
{loadingText}
</>
) : (
children
)}
</ChakraButton>
);
});

View File

@@ -1,25 +0,0 @@
import { Checkbox as ChakraCheckbox } from "@chakra-ui/react"
import * as React from "react"
export interface CheckboxProps extends ChakraCheckbox.RootProps {
icon?: React.ReactNode
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
rootRef?: React.Ref<HTMLLabelElement>
}
export const Checkbox = React.forwardRef<HTMLInputElement, CheckboxProps>(
function Checkbox(props, ref) {
const { icon, children, inputProps, rootRef, ...rest } = props
return (
<ChakraCheckbox.Root ref={rootRef} {...rest}>
<ChakraCheckbox.HiddenInput ref={ref} {...inputProps} />
<ChakraCheckbox.Control>
{icon || <ChakraCheckbox.Indicator />}
</ChakraCheckbox.Control>
{children != null && (
<ChakraCheckbox.Label>{children}</ChakraCheckbox.Label>
)}
</ChakraCheckbox.Root>
)
},
)

View File

@@ -1,17 +0,0 @@
import type { ButtonProps } from "@chakra-ui/react"
import { IconButton as ChakraIconButton } from "@chakra-ui/react"
import * as React from "react"
import { LuX } from "react-icons/lu"
export type CloseButtonProps = ButtonProps
export const CloseButton = React.forwardRef<
HTMLButtonElement,
CloseButtonProps
>(function CloseButton(props, ref) {
return (
<ChakraIconButton variant="ghost" aria-label="Close" ref={ref} {...props}>
{props.children ?? <LuX />}
</ChakraIconButton>
)
})

View File

@@ -1,75 +0,0 @@
"use client";
import type { IconButtonProps } from "@chakra-ui/react";
import { ClientOnly, IconButton, Skeleton } from "@chakra-ui/react";
import type { ThemeProviderProps } from "next-themes";
import { ThemeProvider, useTheme } from "next-themes";
import * as React from "react";
import { LuMoon, LuSun } from "react-icons/lu";
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
export interface ColorModeProviderProps extends ThemeProviderProps {}
export function ColorModeProvider(props: ColorModeProviderProps) {
return <ThemeProvider attribute="class" disableTransitionOnChange {...props} />;
}
export type ColorMode = "light" | "dark";
export interface UseColorModeReturn {
colorMode: ColorMode;
setColorMode: (colorMode: ColorMode) => void;
toggleColorMode: () => void;
}
export function useColorMode(): UseColorModeReturn {
const { resolvedTheme, setTheme } = useTheme();
const toggleColorMode = () => {
setTheme(resolvedTheme === "light" ? "dark" : "light");
};
return {
colorMode: resolvedTheme as ColorMode,
setColorMode: setTheme,
toggleColorMode,
};
}
export function useColorModeValue<T>(light: T, dark: T) {
const { colorMode } = useColorMode();
return colorMode === "dark" ? dark : light;
}
export function ColorModeIcon() {
const { colorMode } = useColorMode();
return colorMode === "dark" ? <LuMoon /> : <LuSun />;
}
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface ColorModeButtonProps extends Omit<IconButtonProps, "aria-label"> {}
export const ColorModeButton = React.forwardRef<HTMLButtonElement, ColorModeButtonProps>(function ColorModeButton(
props,
ref,
) {
const { toggleColorMode } = useColorMode();
return (
<ClientOnly fallback={<Skeleton boxSize="8" />}>
<IconButton
onClick={toggleColorMode}
variant="ghost"
aria-label="Toggle color mode"
size="sm"
ref={ref}
{...props}
css={{
_icon: {
width: "5",
height: "5",
},
}}
>
<ColorModeIcon />
</IconButton>
</ClientOnly>
);
});

View File

@@ -1,62 +0,0 @@
import { Dialog as ChakraDialog, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DialogContentProps extends ChakraDialog.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement>
backdrop?: boolean
}
export const DialogContent = React.forwardRef<
HTMLDivElement,
DialogContentProps
>(function DialogContent(props, ref) {
const {
children,
portalled = true,
portalRef,
backdrop = true,
...rest
} = props
return (
<Portal disabled={!portalled} container={portalRef}>
{backdrop && <ChakraDialog.Backdrop />}
<ChakraDialog.Positioner>
<ChakraDialog.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDialog.Content>
</ChakraDialog.Positioner>
</Portal>
)
})
export const DialogCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDialog.CloseTriggerProps
>(function DialogCloseTrigger(props, ref) {
return (
<ChakraDialog.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref}>
{props.children}
</CloseButton>
</ChakraDialog.CloseTrigger>
)
})
export const DialogRoot = ChakraDialog.Root
export const DialogFooter = ChakraDialog.Footer
export const DialogHeader = ChakraDialog.Header
export const DialogBody = ChakraDialog.Body
export const DialogBackdrop = ChakraDialog.Backdrop
export const DialogTitle = ChakraDialog.Title
export const DialogDescription = ChakraDialog.Description
export const DialogTrigger = ChakraDialog.Trigger
export const DialogActionTrigger = ChakraDialog.ActionTrigger

View File

@@ -1,52 +0,0 @@
import { Drawer as ChakraDrawer, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface DrawerContentProps extends ChakraDrawer.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement>
offset?: ChakraDrawer.ContentProps["padding"]
}
export const DrawerContent = React.forwardRef<
HTMLDivElement,
DrawerContentProps
>(function DrawerContent(props, ref) {
const { children, portalled = true, portalRef, offset, ...rest } = props
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraDrawer.Positioner padding={offset}>
<ChakraDrawer.Content ref={ref} {...rest} asChild={false}>
{children}
</ChakraDrawer.Content>
</ChakraDrawer.Positioner>
</Portal>
)
})
export const DrawerCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraDrawer.CloseTriggerProps
>(function DrawerCloseTrigger(props, ref) {
return (
<ChakraDrawer.CloseTrigger
position="absolute"
top="2"
insetEnd="2"
{...props}
asChild
>
<CloseButton size="sm" ref={ref} />
</ChakraDrawer.CloseTrigger>
)
})
export const DrawerTrigger = ChakraDrawer.Trigger
export const DrawerRoot = ChakraDrawer.Root
export const DrawerFooter = ChakraDrawer.Footer
export const DrawerHeader = ChakraDrawer.Header
export const DrawerBody = ChakraDrawer.Body
export const DrawerBackdrop = ChakraDrawer.Backdrop
export const DrawerDescription = ChakraDrawer.Description
export const DrawerTitle = ChakraDrawer.Title
export const DrawerActionTrigger = ChakraDrawer.ActionTrigger

View File

@@ -1,33 +0,0 @@
import { Field as ChakraField } from "@chakra-ui/react"
import * as React from "react"
export interface FieldProps extends Omit<ChakraField.RootProps, "label"> {
label?: React.ReactNode
helperText?: React.ReactNode
errorText?: React.ReactNode
optionalText?: React.ReactNode
}
export const Field = React.forwardRef<HTMLDivElement, FieldProps>(
function Field(props, ref) {
const { label, children, helperText, errorText, optionalText, ...rest } =
props
return (
<ChakraField.Root ref={ref} {...rest}>
{label && (
<ChakraField.Label>
{label}
<ChakraField.RequiredIndicator fallback={optionalText} />
</ChakraField.Label>
)}
{children}
{helperText && (
<ChakraField.HelperText>{helperText}</ChakraField.HelperText>
)}
{errorText && (
<ChakraField.ErrorText>{errorText}</ChakraField.ErrorText>
)}
</ChakraField.Root>
)
},
)

View File

@@ -1,53 +0,0 @@
import type { BoxProps, InputElementProps } from "@chakra-ui/react"
import { Group, InputElement } from "@chakra-ui/react"
import * as React from "react"
export interface InputGroupProps extends BoxProps {
startElementProps?: InputElementProps
endElementProps?: InputElementProps
startElement?: React.ReactNode
endElement?: React.ReactNode
children: React.ReactElement<InputElementProps>
startOffset?: InputElementProps["paddingStart"]
endOffset?: InputElementProps["paddingEnd"]
}
export const InputGroup = React.forwardRef<HTMLDivElement, InputGroupProps>(
function InputGroup(props, ref) {
const {
startElement,
startElementProps,
endElement,
endElementProps,
children,
startOffset = "6px",
endOffset = "6px",
...rest
} = props
const child =
React.Children.only<React.ReactElement<InputElementProps>>(children)
return (
<Group ref={ref} {...rest}>
{startElement && (
<InputElement pointerEvents="none" {...startElementProps}>
{startElement}
</InputElement>
)}
{React.cloneElement(child, {
...(startElement && {
ps: `calc(var(--input-height) - ${startOffset})`,
}),
...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }),
...children.props,
})}
{endElement && (
<InputElement placement="end" {...endElementProps}>
{endElement}
</InputElement>
)}
</Group>
)
},
)

View File

@@ -1,59 +0,0 @@
import { Popover as ChakraPopover, Portal } from "@chakra-ui/react"
import { CloseButton } from "./close-button"
import * as React from "react"
interface PopoverContentProps extends ChakraPopover.ContentProps {
portalled?: boolean
portalRef?: React.RefObject<HTMLElement>
}
export const PopoverContent = React.forwardRef<
HTMLDivElement,
PopoverContentProps
>(function PopoverContent(props, ref) {
const { portalled = true, portalRef, ...rest } = props
return (
<Portal disabled={!portalled} container={portalRef}>
<ChakraPopover.Positioner>
<ChakraPopover.Content ref={ref} {...rest} />
</ChakraPopover.Positioner>
</Portal>
)
})
export const PopoverArrow = React.forwardRef<
HTMLDivElement,
ChakraPopover.ArrowProps
>(function PopoverArrow(props, ref) {
return (
<ChakraPopover.Arrow {...props} ref={ref}>
<ChakraPopover.ArrowTip />
</ChakraPopover.Arrow>
)
})
export const PopoverCloseTrigger = React.forwardRef<
HTMLButtonElement,
ChakraPopover.CloseTriggerProps
>(function PopoverCloseTrigger(props, ref) {
return (
<ChakraPopover.CloseTrigger
position="absolute"
top="1"
insetEnd="1"
{...props}
asChild
ref={ref}
>
<CloseButton size="sm" />
</ChakraPopover.CloseTrigger>
)
})
export const PopoverTitle = ChakraPopover.Title
export const PopoverDescription = ChakraPopover.Description
export const PopoverFooter = ChakraPopover.Footer
export const PopoverHeader = ChakraPopover.Header
export const PopoverRoot = ChakraPopover.Root
export const PopoverBody = ChakraPopover.Body
export const PopoverTrigger = ChakraPopover.Trigger

View File

@@ -1,15 +0,0 @@
"use client"
import { ChakraProvider, defaultSystem } from "@chakra-ui/react"
import {
ColorModeProvider,
type ColorModeProviderProps,
} from "./color-mode"
export function Provider(props: ColorModeProviderProps) {
return (
<ChakraProvider value={defaultSystem}>
<ColorModeProvider {...props} />
</ChakraProvider>
)
}

View File

@@ -1,24 +0,0 @@
import { RadioGroup as ChakraRadioGroup } from "@chakra-ui/react"
import * as React from "react"
export interface RadioProps extends ChakraRadioGroup.ItemProps {
rootRef?: React.Ref<HTMLDivElement>
inputProps?: React.InputHTMLAttributes<HTMLInputElement>
}
export const Radio = React.forwardRef<HTMLInputElement, RadioProps>(
function Radio(props, ref) {
const { children, inputProps, rootRef, ...rest } = props
return (
<ChakraRadioGroup.Item ref={rootRef} {...rest}>
<ChakraRadioGroup.ItemHiddenInput ref={ref} {...inputProps} />
<ChakraRadioGroup.ItemIndicator />
{children && (
<ChakraRadioGroup.ItemText>{children}</ChakraRadioGroup.ItemText>
)}
</ChakraRadioGroup.Item>
)
},
)
export const RadioGroup = ChakraRadioGroup.Root

View File

@@ -1,82 +0,0 @@
import { Slider as ChakraSlider, For, HStack } from "@chakra-ui/react"
import * as React from "react"
export interface SliderProps extends ChakraSlider.RootProps {
marks?: Array<number | { value: number; label: React.ReactNode }>
label?: React.ReactNode
showValue?: boolean
}
export const Slider = React.forwardRef<HTMLDivElement, SliderProps>(
function Slider(props, ref) {
const { marks: marksProp, label, showValue, ...rest } = props
const value = props.defaultValue ?? props.value
const marks = marksProp?.map((mark) => {
if (typeof mark === "number") return { value: mark, label: undefined }
return mark
})
const hasMarkLabel = !!marks?.some((mark) => mark.label)
return (
<ChakraSlider.Root ref={ref} thumbAlignment="center" {...rest}>
{label && !showValue && (
<ChakraSlider.Label>{label}</ChakraSlider.Label>
)}
{label && showValue && (
<HStack justify="space-between">
<ChakraSlider.Label>{label}</ChakraSlider.Label>
<ChakraSlider.ValueText />
</HStack>
)}
<ChakraSlider.Control data-has-mark-label={hasMarkLabel || undefined}>
<ChakraSlider.Track>
<ChakraSlider.Range />
</ChakraSlider.Track>
<SliderThumbs value={value} />
<SliderMarks marks={marks} />
</ChakraSlider.Control>
</ChakraSlider.Root>
)
},
)
function SliderThumbs(props: { value?: number[] }) {
const { value } = props
return (
<For each={value}>
{(_, index) => (
<ChakraSlider.Thumb key={index} index={index}>
<ChakraSlider.HiddenInput />
</ChakraSlider.Thumb>
)}
</For>
)
}
interface SliderMarksProps {
marks?: Array<number | { value: number; label: React.ReactNode }>
}
const SliderMarks = React.forwardRef<HTMLDivElement, SliderMarksProps>(
function SliderMarks(props, ref) {
const { marks } = props
if (!marks?.length) return null
return (
<ChakraSlider.MarkerGroup ref={ref}>
{marks.map((mark, index) => {
const value = typeof mark === "number" ? mark : mark.value
const label = typeof mark === "number" ? undefined : mark.label
return (
<ChakraSlider.Marker key={index} value={value}>
<ChakraSlider.MarkerIndicator />
{label}
</ChakraSlider.Marker>
)
})}
</ChakraSlider.MarkerGroup>
)
},
)

View File

@@ -1,46 +0,0 @@
import { Tooltip as ChakraTooltip, Portal } from "@chakra-ui/react"
import * as React from "react"
export interface TooltipProps extends ChakraTooltip.RootProps {
showArrow?: boolean
portalled?: boolean
portalRef?: React.RefObject<HTMLElement>
content: React.ReactNode
contentProps?: ChakraTooltip.ContentProps
disabled?: boolean
}
export const Tooltip = React.forwardRef<HTMLDivElement, TooltipProps>(
function Tooltip(props, ref) {
const {
showArrow,
children,
disabled,
portalled = true,
content,
contentProps,
portalRef,
...rest
} = props
if (disabled) return children
return (
<ChakraTooltip.Root {...rest}>
<ChakraTooltip.Trigger asChild>{children}</ChakraTooltip.Trigger>
<Portal disabled={!portalled} container={portalRef}>
<ChakraTooltip.Positioner>
<ChakraTooltip.Content ref={ref} {...contentProps}>
{showArrow && (
<ChakraTooltip.Arrow>
<ChakraTooltip.ArrowTip />
</ChakraTooltip.Arrow>
)}
{content}
</ChakraTooltip.Content>
</ChakraTooltip.Positioner>
</Portal>
</ChakraTooltip.Root>
)
},
)

3
src/lib/auth.ts Normal file
View File

@@ -0,0 +1,3 @@
export function isAuthenticated() {
return true;
}

4
src/middleware.ts Normal file
View File

@@ -0,0 +1,4 @@
export { default } from "next-auth/middleware";
export const config = {
matcher: ["/admin/panel"],
};