Compare commits

...

3 Commits

Author SHA1 Message Date
d132b4971e Feat: added switch theme btn to main page 2025-02-05 17:00:38 +03:00
f68e290f58 Feat: using router and added footer 2025-02-03 12:29:10 +03:00
ed9d5d1b9f Feat: authentication 2025-02-03 11:54:45 +03:00
12 changed files with 176 additions and 94 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -14,6 +14,7 @@
"@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",

View File

@@ -1,9 +0,0 @@
"use server";
import { redirect } from "next/navigation";
export async function login(formData: { name: string; password: string }) {
console.log("data");
console.log(formData);
redirect("/admin/panel");
}

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>
);
}

View File

@@ -1,60 +1,9 @@
"use client";
"use server";
import { Button, Card, Flex, PasswordInput, Text, TextInput } from "@mantine/core";
import { hasLength, useForm } from "@mantine/form";
import { login } from "./actions";
interface FormValues {
name: string;
password: string;
}
import { redirect } from "next/navigation";
const AdminPage = () => {
const form = useForm<FormValues>({
mode: "uncontrolled",
initialValues: {
name: "",
password: "",
},
validate: {
name: hasLength({ min: 5 }, "Too short"),
password: hasLength({ min: 5 }, "Too short"),
},
});
return (
<Flex w="100vw" h="100vh" justify="center" align="center">
<Card shadow="md" w="400" radius="md">
<form onSubmit={form.onSubmit(login)}>
<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>
</form>
</Card>
</Flex>
);
redirect("/admin/panel");
};
export default AdminPage;

View File

@@ -1,18 +1,25 @@
"use client";
import { ActionIcon, AppShell, Burger, Flex, Group, Skeleton, useMantineColorScheme } from "@mantine/core";
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";
const AdminLayout = ({
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>
@@ -32,10 +39,16 @@ const AdminLayout = ({
.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 AdminLayout;
export default AdminPanelLayout;

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

View File

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

View File

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

View File

@@ -1,14 +1,4 @@
import { isAuthenticated } from "@/lib/auth";
import { NextRequest, NextResponse } from "next/server";
export { default } from "next-auth/middleware";
export const config = {
matcher: "/admin/:path*",
matcher: ["/admin/panel"],
};
export function middleware(request: NextRequest) {
const { pathname } = request.nextUrl;
if (pathname === "/admin" || isAuthenticated()) {
return NextResponse.next();
}
return NextResponse.redirect(new URL("/admin", request.url));
}