Compare commits
5 Commits
d97cafa9ec
...
dev
| Author | SHA1 | Date | |
|---|---|---|---|
| d132b4971e | |||
| f68e290f58 | |||
| ed9d5d1b9f | |||
| 352e191eab | |||
| 2a9a49c5ec |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -41,4 +41,8 @@ yarn-error.log*
|
||||
next-env.d.ts
|
||||
|
||||
# repomix
|
||||
repomix-output.txt
|
||||
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"
|
||||
}
|
||||
@@ -10,12 +10,15 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@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-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": {
|
||||
"@eslint/eslintrc": "^3",
|
||||
|
||||
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 +1,10 @@
|
||||
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.scss";
|
||||
import Providers from "./providers";
|
||||
|
||||
const geistSans = Geist({
|
||||
variable: "--font-geist-sans",
|
||||
@@ -19,18 +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`}>
|
||||
<MantineProvider>{children}</MantineProvider>
|
||||
<MantineProvider>
|
||||
<Providers session={session}>{children}</Providers>
|
||||
</MantineProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { AppShell, Burger, Button, Group, Skeleton, useMantineColorScheme } from "@mantine/core";
|
||||
import { ActionIcon, AppShell, Burger, Flex, Group, Skeleton, useMantineColorScheme } from "@mantine/core";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import { LuSun } from "react-icons/lu";
|
||||
import { LuMoon, LuSun } from "react-icons/lu";
|
||||
|
||||
export default function Home() {
|
||||
const [opened, { toggle }] = useDisclosure();
|
||||
@@ -14,10 +14,22 @@ export default function Home() {
|
||||
padding="md"
|
||||
>
|
||||
<AppShell.Header>
|
||||
<Group h="100%" px="md">
|
||||
<Burger opened={opened} onClick={toggle} size="sm" hiddenFrom="sm" />
|
||||
<div>Logo</div>
|
||||
</Group>
|
||||
<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)
|
||||
@@ -25,13 +37,6 @@ export default function Home() {
|
||||
.map((_, index) => (
|
||||
<Skeleton key={index} h={28} mt="sm" animate={false} />
|
||||
))}
|
||||
<Button
|
||||
onClick={() => {
|
||||
setColorScheme(colorScheme === "light" ? "dark" : "light");
|
||||
}}
|
||||
>
|
||||
<LuSun />
|
||||
</Button>
|
||||
</AppShell.Navbar>
|
||||
<AppShell.Main>Main</AppShell.Main>
|
||||
<AppShell.Footer>
|
||||
|
||||
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>;
|
||||
}
|
||||
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"],
|
||||
};
|
||||
Reference in New Issue
Block a user