Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d132b4971e | |||
| f68e290f58 | |||
| ed9d5d1b9f | |||
| 352e191eab | |||
| 2a9a49c5ec | |||
| d97cafa9ec | |||
| c8a42598bf | |||
| bc5d6097db |
7
.gitignore
vendored
7
.gitignore
vendored
@@ -39,3 +39,10 @@ yarn-error.log*
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
# repomix
|
||||
repomix-output.txt
|
||||
|
||||
# vscode
|
||||
.vscode/*
|
||||
!.vscode/settings.json
|
||||
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"scss.lint.unknownAtRules": "ignore"
|
||||
}
|
||||
19
.woodpecker/pipeline.yml
Normal file
19
.woodpecker/pipeline.yml
Normal file
@@ -0,0 +1,19 @@
|
||||
steps:
|
||||
- name: build
|
||||
image: docker:latest
|
||||
commands:
|
||||
- docker build -t nwblog:latest .
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
when:
|
||||
- branch: [master, deploy]
|
||||
- event: [push, manual]
|
||||
- name: deploy
|
||||
image: docker:latest
|
||||
commands:
|
||||
- docker run -d -p 127.0.0.1:25010:3000 --restart always --name nwblog nwblog:latest
|
||||
volumes:
|
||||
- /var/run/docker.sock:/var/run/docker.sock
|
||||
when:
|
||||
- branch: deploy
|
||||
- event: manual
|
||||
@@ -1,9 +1,14 @@
|
||||
import type { NextConfig } from "next";
|
||||
import path from "node:path";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
output: "standalone",
|
||||
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;`,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
21
package.json
21
package.json
@@ -9,23 +9,28 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@chakra-ui/react": "^3.4.0",
|
||||
"@emotion/react": "^11.14.0",
|
||||
"@mantine/core": "^7.16.1",
|
||||
"@mantine/form": "^7.16.1",
|
||||
"@mantine/hooks": "^7.16.1",
|
||||
"@mantine/modals": "^7.16.1",
|
||||
"next": "15.1.5",
|
||||
"next-themes": "^0.4.4",
|
||||
"next-auth": "^4.24.11",
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-icons": "^5.4.0"
|
||||
"react-icons": "^5.4.0",
|
||||
"zod": "^3.24.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5",
|
||||
"@eslint/eslintrc": "^3",
|
||||
"@types/node": "^20",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"postcss": "^8",
|
||||
"tailwindcss": "^3.4.1",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "15.1.5",
|
||||
"@eslint/eslintrc": "^3"
|
||||
"postcss": "^8.5.1",
|
||||
"postcss-preset-mantine": "^1.17.0",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"sass-embedded": "^1.83.4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
|
||||
14
postcss.config.cjs
Normal file
14
postcss.config.cjs
Normal 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",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
/** @type {import('postcss-load-config').Config} */
|
||||
const config = {
|
||||
plugins: {
|
||||
tailwindcss: {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
57
src/_mantine.scss
Normal file
57
src/_mantine.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
0
src/app/admin/admin.module.scss
Normal file
0
src/app/admin/admin.module.scss
Normal file
83
src/app/admin/login/page.tsx
Normal file
83
src/app/admin/login/page.tsx
Normal 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
9
src/app/admin/page.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
"use server";
|
||||
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
const AdminPage = () => {
|
||||
redirect("/admin/panel");
|
||||
};
|
||||
|
||||
export default AdminPage;
|
||||
54
src/app/admin/panel/layout.tsx
Normal file
54
src/app/admin/panel/layout.tsx
Normal 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;
|
||||
6
src/app/admin/panel/page.tsx
Normal file
6
src/app/admin/panel/page.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
"use client";
|
||||
|
||||
const PanelPage = () => {
|
||||
return <div>Panel</div>;
|
||||
};
|
||||
export default PanelPage;
|
||||
6
src/app/admin/panel/panel.module.scss
Normal file
6
src/app/admin/panel/panel.module.scss
Normal 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));
|
||||
}
|
||||
}
|
||||
33
src/app/api/auth/[...nextauth]/route.ts
Normal file
33
src/app/api/auth/[...nextauth]/route.ts
Normal 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
3
src/app/api/route.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export async function GET() {
|
||||
return Response.json({ test: 1 });
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function Blog() {
|
||||
return <h1>Blog</h1>;
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
0
src/app/globals.scss
Normal file
0
src/app/globals.scss
Normal file
@@ -1,7 +1,10 @@
|
||||
import { Provider } from "@/components/ui/provider";
|
||||
import { ColorSchemeScript, MantineProvider } from "@mantine/core";
|
||||
import "@mantine/core/styles.css";
|
||||
import type { Metadata } from "next";
|
||||
import { getSession } from "next-auth/react";
|
||||
import { Geist, Geist_Mono } from "next/font/google";
|
||||
import "./globals.css";
|
||||
import "./globals.scss";
|
||||
import Providers from "./providers";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -18,15 +21,21 @@ export const metadata: Metadata = {
|
||||
description: "Generated by create next app",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
export default async function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
const session = await getSession();
|
||||
return (
|
||||
<html lang="ru" suppressHydrationWarning>
|
||||
<head>
|
||||
<ColorSchemeScript />
|
||||
</head>
|
||||
<body className={`${geistSans.variable} ${geistMono.variable} antialiased`}>
|
||||
<Provider>{children}</Provider>
|
||||
<MantineProvider>
|
||||
<Providers session={session}>{children}</Providers>
|
||||
</MantineProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,11 +1,47 @@
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { VStack } from "@chakra-ui/react";
|
||||
"use client";
|
||||
|
||||
import { ActionIcon, AppShell, Burger, Flex, Group, Skeleton, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { LuMoon, LuSun } from "react-icons/lu";
|
||||
|
||||
export default function Home() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
const { setColorScheme, colorScheme } = useMantineColorScheme();
|
||||
return (
|
||||
<VStack>
|
||||
<Button>Test 1</Button>
|
||||
<Button>Test 2</Button>
|
||||
</VStack>
|
||||
<AppShell
|
||||
header={{ height: 60 }}
|
||||
navbar={{ width: 300, breakpoint: "sm", collapsed: { mobile: !opened } }}
|
||||
padding="md"
|
||||
>
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
||||
6
src/app/providers.tsx
Normal file
6
src/app/providers.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
"use client";
|
||||
import type { Session } from "next-auth";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
export default function Providers({ session, children }: { session: Session | null; children: React.ReactNode }) {
|
||||
return <SessionProvider session={session}>{children}</SessionProvider>;
|
||||
}
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -1,40 +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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
})
|
||||
@@ -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>
|
||||
);
|
||||
});
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
@@ -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>
|
||||
)
|
||||
},
|
||||
)
|
||||
@@ -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
3
src/lib/auth.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export function isAuthenticated() {
|
||||
return true;
|
||||
}
|
||||
4
src/middleware.ts
Normal file
4
src/middleware.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { default } from "next-auth/middleware";
|
||||
export const config = {
|
||||
matcher: ["/admin/panel"],
|
||||
};
|
||||
@@ -1,18 +0,0 @@
|
||||
import type { Config } from "tailwindcss";
|
||||
|
||||
export default {
|
||||
content: [
|
||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
background: "var(--background)",
|
||||
foreground: "var(--foreground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
} satisfies Config;
|
||||
Reference in New Issue
Block a user