mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Add login
This commit is contained in:
70
package-lock.json
generated
70
package-lock.json
generated
@@ -8,12 +8,18 @@
|
||||
"name": "torrent_frontend",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.4.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "14.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-responsive-masonry": "^2.2.0",
|
||||
"swr": "^2.2.5",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -108,6 +114,14 @@
|
||||
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "3.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.4.0.tgz",
|
||||
"integrity": "sha512-+oAqK3okmoEDnvUkJ3N/mvNMeeMv5Apgy1jkoRmlaaAF4vBgcJs9tHvtXU7VE4DvPosvAUUkPOaNFunzt1dbgA==",
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanwhocodes/config-array": {
|
||||
"version": "0.11.14",
|
||||
"resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
|
||||
@@ -447,6 +461,11 @@
|
||||
"tslib": "^2.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/js-cookie": {
|
||||
"version": "3.0.6",
|
||||
"resolved": "https://registry.npmjs.org/@types/js-cookie/-/js-cookie-3.0.6.tgz",
|
||||
"integrity": "sha512-wkw9yd1kEXOPnvEeEV1Go1MmxtBJL0RR79aOTAApecWFVu7w0NNXNqhcWgvw2YgZDYadliXkl14pa3WXw5jlCQ=="
|
||||
},
|
||||
"node_modules/@types/json5": {
|
||||
"version": "0.0.29",
|
||||
"resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
|
||||
@@ -2880,6 +2899,14 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/js-cookie": {
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz",
|
||||
"integrity": "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
@@ -2942,6 +2969,14 @@
|
||||
"node": ">=4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/jwt-decode": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/jwt-decode/-/jwt-decode-4.0.0.tgz",
|
||||
"integrity": "sha512-+KJGIyHgkGuIq3IEBNftfhW/LfWhXUIY6OmyVWjliu5KH1y0fw7VQ8YndE2O4qZdMSd9SqbnC8GOcZEy0Om7sA==",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/keyv": {
|
||||
"version": "4.5.4",
|
||||
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
|
||||
@@ -3749,6 +3784,21 @@
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.51.4",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
|
||||
"integrity": "sha512-V14i8SEkh+V1gs6YtD0hdHYnoL4tp/HX/A45wWQN15CYr9bFRmmRdYStSO5L65lCCZRF+kYiSKhm9alqbcdiVA==",
|
||||
"engines": {
|
||||
"node": ">=12.22.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@@ -4346,6 +4396,18 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/swr": {
|
||||
"version": "2.2.5",
|
||||
"resolved": "https://registry.npmjs.org/swr/-/swr-2.2.5.tgz",
|
||||
"integrity": "sha512-QtxqyclFeAsxEUeZIYmsaQ0UjimSq1RZ9Un7I68/0ClKK/U3LoyQunwkQfJZr2fc22DfIXLNDc2wFyTEikCUpg==",
|
||||
"dependencies": {
|
||||
"client-only": "^0.0.1",
|
||||
"use-sync-external-store": "^1.2.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.11.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tailwindcss": {
|
||||
"version": "3.4.3",
|
||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz",
|
||||
@@ -4606,6 +4668,14 @@
|
||||
"punycode": "^2.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/use-sync-external-store": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz",
|
||||
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==",
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
|
||||
@@ -9,12 +9,18 @@
|
||||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@hookform/resolvers": "^3.4.0",
|
||||
"@types/js-cookie": "^3.0.6",
|
||||
"clsx": "^2.1.1",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"next": "14.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18",
|
||||
"react-hook-form": "^7.51.4",
|
||||
"react-responsive-masonry": "^2.2.0",
|
||||
"swr": "^2.2.5",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -1,3 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import {
|
||||
LoginForm,
|
||||
loginFormFieldNames,
|
||||
loginFormSchema,
|
||||
} from "@/entities/user";
|
||||
import { UserService } from "@/entities/user/user";
|
||||
import { Modal } from "@/shared/ui";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { SubmitHandler, useForm } from "react-hook-form";
|
||||
import { mutate } from "swr";
|
||||
|
||||
export default function Login() {
|
||||
return <>123</>;
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<LoginForm>({ resolver: zodResolver(loginFormSchema) });
|
||||
|
||||
const router = useRouter();
|
||||
|
||||
const onSubmit: SubmitHandler<LoginForm> = async (data) => {
|
||||
const userInfo = await UserService.Login(data);
|
||||
mutate("user", userInfo);
|
||||
router.back();
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<div className="">
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="flex flex-col items-center justify-evenly"
|
||||
>
|
||||
<h2 className="pb-4 text-4xl">.Torrent</h2>
|
||||
{(["username", "password"] as ("username" | "password")[]).map(
|
||||
(field) => (
|
||||
<label
|
||||
className="flex flex-col items-start relative w-64 py-1"
|
||||
key={field}
|
||||
>
|
||||
<input
|
||||
{...register(field)}
|
||||
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
||||
placeholder=" "
|
||||
autoComplete="off"
|
||||
/>
|
||||
<span
|
||||
className="peer-focus/search:opacity-0
|
||||
peer-[:not(:placeholder-shown)]/search:opacity-0
|
||||
transition-opacity h-0 flex items-center relative bottom-5 left-4
|
||||
text-lg"
|
||||
>
|
||||
{loginFormFieldNames[field]}
|
||||
</span>
|
||||
<p className="text-sm text-err w-full text-center">
|
||||
{errors[field]?.message}
|
||||
</p>
|
||||
</label>
|
||||
)
|
||||
)}
|
||||
|
||||
<input
|
||||
type="submit"
|
||||
value="Войти"
|
||||
className="bg-ac0 mt-2 p-1 px-4 rounded-lg"
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export default function CatchAll() {
|
||||
return null;
|
||||
}
|
||||
@@ -15,6 +15,8 @@
|
||||
--color-ac1: #fabd2f;
|
||||
--color-ac2: #8ec07c;
|
||||
|
||||
--color-err: #cc241d;
|
||||
|
||||
--app-width: 70%;
|
||||
font-size: calc((100vw / 1920) * 20);
|
||||
}
|
||||
@@ -31,6 +33,8 @@
|
||||
--color-ac0: #076678;
|
||||
--color-ac1: #b57614;
|
||||
--color-ac2: #427b58;
|
||||
|
||||
--color-err: #cc241d;
|
||||
}
|
||||
|
||||
html,
|
||||
@@ -45,7 +49,7 @@ body {
|
||||
transition-property: color, background-color, border-color;
|
||||
transition-duration: 0.3s;
|
||||
|
||||
/* overflow: hidden; */
|
||||
overflow: hidden;
|
||||
color: var(--color-fg1);
|
||||
background-color: var(--color-bg0);
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ export default function RootLayout({
|
||||
<ThemeProvider enableSystem={false} defaultTheme="light">
|
||||
{auth}
|
||||
<Header />
|
||||
<div className="w-full h-full max-w-[var(--app-width)] m-auto">
|
||||
<div className="w-full h-full max-w-[var(--app-width)] m-auto overflow-y-auto">
|
||||
{children}
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default function Login() {
|
||||
return <>login page</>;
|
||||
redirect("/");
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { HTTPService } from "@/shared/http/httpService";
|
||||
import { HTTPService } from "@/shared/utils/http";
|
||||
import { gameCardsSchema, GameCardType } from "./schemas/gameCard";
|
||||
import { gameSchema, GameType } from "./schemas/game";
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { z } from "zod";
|
||||
|
||||
export const gameCardSchema = z
|
||||
.object({
|
||||
id: z.number(),
|
||||
id: z.number().positive(),
|
||||
title: z.string().min(3),
|
||||
cover: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
|
||||
16
src/entities/user/index.ts
Normal file
16
src/entities/user/index.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import {
|
||||
loginFormSchema,
|
||||
loginFormFieldNames,
|
||||
LoginForm,
|
||||
} from "./schemas/auth";
|
||||
import { userSchema, User } from "./schemas/user";
|
||||
import { UserService } from "./user";
|
||||
|
||||
export {
|
||||
loginFormSchema,
|
||||
loginFormFieldNames,
|
||||
UserService,
|
||||
userSchema,
|
||||
type User,
|
||||
type LoginForm,
|
||||
};
|
||||
30
src/entities/user/schemas/auth.ts
Normal file
30
src/entities/user/schemas/auth.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { z } from "zod";
|
||||
import { userSchema } from "./user";
|
||||
|
||||
export const loginFormSchema = z.object({
|
||||
username: z.string().min(3, "Логин слишком короткий"),
|
||||
password: z.string().min(3, "Пароль слишком короткий"),
|
||||
});
|
||||
export const loginFormFieldNames = {
|
||||
username: "Логин",
|
||||
password: "Пароль",
|
||||
};
|
||||
export type LoginForm = z.infer<typeof loginFormSchema>;
|
||||
|
||||
export const tokenResponseSchema = z
|
||||
.object({
|
||||
access_token: z.string(),
|
||||
token_type: z.string(),
|
||||
})
|
||||
.transform((tokenResponse) => tokenResponse.access_token);
|
||||
export type TokenResponse = z.infer<typeof tokenResponseSchema>;
|
||||
|
||||
export const tokenDataSchema = userSchema.merge(
|
||||
z.object({
|
||||
expire: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
})
|
||||
);
|
||||
export type TokenData = z.infer<typeof tokenDataSchema>;
|
||||
8
src/entities/user/schemas/user.ts
Normal file
8
src/entities/user/schemas/user.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const userSchema = z.object({
|
||||
id: z.number().positive(),
|
||||
name: z.string().min(3),
|
||||
email: z.string().min(3),
|
||||
});
|
||||
export type User = z.infer<typeof userSchema>;
|
||||
48
src/entities/user/user.ts
Normal file
48
src/entities/user/user.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { HTTPService } from "@/shared/utils/http";
|
||||
import {
|
||||
LoginForm,
|
||||
TokenData,
|
||||
tokenDataSchema,
|
||||
TokenResponse,
|
||||
tokenResponseSchema,
|
||||
} from "./schemas/auth";
|
||||
import { jwtDecode } from "jwt-decode";
|
||||
import Cookies from "js-cookie";
|
||||
|
||||
export abstract class UserService {
|
||||
public static async Login(loginForm: LoginForm) {
|
||||
const accessToken = await HTTPService.post<TokenResponse>(
|
||||
"/auth",
|
||||
new URLSearchParams(Object.entries(loginForm)),
|
||||
tokenResponseSchema,
|
||||
{
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
}
|
||||
);
|
||||
if (accessToken) {
|
||||
const tokenData = this.DecodeToken(accessToken);
|
||||
if (tokenData) {
|
||||
Cookies.set("access-token", accessToken, {
|
||||
secure: true,
|
||||
expires: tokenData.expire,
|
||||
});
|
||||
return tokenData;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static IdentifyYourself(): TokenData | undefined {
|
||||
const token = Cookies.get("access-token");
|
||||
if (token) {
|
||||
return this.DecodeToken(token);
|
||||
}
|
||||
}
|
||||
|
||||
public static DecodeToken(token: string): TokenData | undefined {
|
||||
const tokenPayload = jwtDecode(token);
|
||||
const parseResult = tokenDataSchema.safeParse(tokenPayload);
|
||||
if (parseResult.success) {
|
||||
return parseResult.data;
|
||||
} else console.error("JWT payload broken - " + parseResult.error);
|
||||
}
|
||||
}
|
||||
3
src/features/userActivities/index.ts
Normal file
3
src/features/userActivities/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { UserActivities } from "./userActivities";
|
||||
|
||||
export { UserActivities };
|
||||
29
src/features/userActivities/userActivities.tsx
Normal file
29
src/features/userActivities/userActivities.tsx
Normal file
@@ -0,0 +1,29 @@
|
||||
"use client";
|
||||
|
||||
import { UserService } from "@/entities/user";
|
||||
import { PersonIcon } from "@/shared/assets/icons";
|
||||
import Link from "next/link";
|
||||
import useSWR from "swr";
|
||||
|
||||
export const UserActivities = () => {
|
||||
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
||||
|
||||
return (
|
||||
<>
|
||||
<PersonIcon className="mr-1 h-4 w-4" />
|
||||
{me && (
|
||||
<span className="group/login cursor-pointer flex items-center">
|
||||
<span className="group-hover/login:underline">{me.name}</span>
|
||||
</span>
|
||||
)}
|
||||
{!me && (
|
||||
<Link
|
||||
href="/login"
|
||||
className="group/login cursor-pointer flex items-center"
|
||||
>
|
||||
<span className="group-hover/login:underline">Войти</span>
|
||||
</Link>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,34 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export abstract class HTTPService {
|
||||
public static async get<Z>(
|
||||
url: string,
|
||||
schema: z.ZodTypeAny
|
||||
): Promise<Z | null> {
|
||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((r) => {
|
||||
if (r && r.ok) return r;
|
||||
else throw Error("Response ok = false");
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const parseResult = schema.safeParse(d);
|
||||
if (parseResult.success) {
|
||||
return parseResult.data as Z;
|
||||
} else {
|
||||
console.error(parseResult.error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
3
src/shared/ui/index.ts
Normal file
3
src/shared/ui/index.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
import { Modal } from "./modal";
|
||||
|
||||
export { Modal };
|
||||
33
src/shared/ui/modal.tsx
Normal file
33
src/shared/ui/modal.tsx
Normal file
@@ -0,0 +1,33 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const Modal = ({ children }: { children: React.ReactNode }) => {
|
||||
const [closing, setClosing] = useState(false);
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
!closing && "animate-fadeIn",
|
||||
closing && "animate-fadeOut opacity-0",
|
||||
"flex items-center justify-around",
|
||||
"absolute z-20 left-0 w-full h-full bg-[#000000c5]"
|
||||
)}
|
||||
onClick={() => {
|
||||
setClosing(true);
|
||||
setTimeout(() => router.back(), 500);
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="rounded-lg bg-bg1 w-fit h-fit p-6"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
53
src/shared/utils/http.ts
Normal file
53
src/shared/utils/http.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export abstract class HTTPService {
|
||||
public static async get<Z>(
|
||||
url: string,
|
||||
schema: z.ZodTypeAny
|
||||
): Promise<Z | null> {
|
||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((r) => {
|
||||
if (r && r.ok) return r;
|
||||
else throw Error("Response ok = false");
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => schema.parse(d))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
public static async post<Z>(
|
||||
url: string,
|
||||
body: BodyInit,
|
||||
schema: z.ZodTypeAny,
|
||||
headers?: HeadersInit
|
||||
): Promise<Z | null> {
|
||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
...headers,
|
||||
},
|
||||
body: body,
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((r) => {
|
||||
if (r && r.ok) return r;
|
||||
else throw Error("Response ok = false");
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => schema.parse(d))
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,3 @@
|
||||
import { getYouTubeID } from "./get_youtube_id";
|
||||
import { getYouTubeID } from "./getYoutubeId";
|
||||
|
||||
export { getYouTubeID };
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
"use client";
|
||||
|
||||
import { SchemeSwitch } from "@/features/colorSchemeSwitch";
|
||||
import { PersonIcon, SearchIcon } from "@/shared/assets/icons";
|
||||
import { SearchIcon } from "@/shared/assets/icons";
|
||||
import { MobileMenu } from "./mobileMenu/mobileMenu";
|
||||
import Link from "next/link";
|
||||
import { useSelectedLayoutSegment } from "next/navigation";
|
||||
import clsx from "clsx";
|
||||
import { UserActivities } from "@/features/userActivities";
|
||||
|
||||
const sections = [
|
||||
{ title: "Игры", href: "games" },
|
||||
@@ -17,7 +18,7 @@ export const Header = () => {
|
||||
const currentPageName = useSelectedLayoutSegment();
|
||||
|
||||
return (
|
||||
<header className="w-full h-20 bg-bg1 sticky top-0 shadow-xl">
|
||||
<header className="w-full h-20 z-10 bg-bg1 sticky top-0 shadow-xl">
|
||||
<div
|
||||
className="w-full h-full max-w-[var(--app-width)] m-auto px-5
|
||||
flex items-center justify-between"
|
||||
@@ -28,7 +29,7 @@ export const Header = () => {
|
||||
</div>
|
||||
<Link href="/">.Torrent</Link>
|
||||
</h1>
|
||||
<div className="hidden text-2xl dsk:block">
|
||||
<div className="hidden text-2xl lp:block">
|
||||
{sections.map((section) => (
|
||||
<Link
|
||||
key={section.title}
|
||||
@@ -45,13 +46,7 @@ export const Header = () => {
|
||||
<div className="flex flex-col items-end">
|
||||
<span className="flex items-center mb-1 ">
|
||||
<SchemeSwitch />
|
||||
<Link
|
||||
href="/login"
|
||||
className="group/login cursor-pointer flex items-center"
|
||||
>
|
||||
<PersonIcon className="mr-1 h-4 w-4" />
|
||||
<span className="group-hover/login:underline">Войти</span>
|
||||
</Link>
|
||||
<UserActivities />
|
||||
</span>
|
||||
<label className="flex flex-col items-start relative w-36">
|
||||
<input
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import clsx from "clsx";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
import Masonry, { ResponsiveMasonry } from "react-responsive-masonry";
|
||||
import { boolean } from "zod";
|
||||
|
||||
export const Section = ({
|
||||
name,
|
||||
@@ -16,6 +19,9 @@ export const Section = ({
|
||||
children: React.ReactNode;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const [loaded, setLoaded] = useState<boolean>(false);
|
||||
|
||||
useEffect(() => setLoaded(true), []);
|
||||
|
||||
return (
|
||||
<section className="w-full h-fit p-2 mb-20 pt-8">
|
||||
@@ -27,7 +33,13 @@ export const Section = ({
|
||||
{name}
|
||||
</h2>
|
||||
)}
|
||||
<ResponsiveMasonry columnsCountBreakPoints={{ 0: 1, 640: 2, 1024: 3 }}>
|
||||
<ResponsiveMasonry
|
||||
className={clsx(
|
||||
"transition-opacity duration-300 opacity-0",
|
||||
loaded && "opacity-100"
|
||||
)}
|
||||
columnsCountBreakPoints={{ 0: 1, 640: 2, 1024: 3 }}
|
||||
>
|
||||
<Masonry gutter="1rem">{children}</Masonry>
|
||||
</ResponsiveMasonry>
|
||||
{link && invite_text && (
|
||||
|
||||
@@ -24,7 +24,22 @@ const config: Config = {
|
||||
ac0: "var(--color-ac0)",
|
||||
ac1: "var(--color-ac1)",
|
||||
ac2: "var(--color-ac2)",
|
||||
err: "var(--color-err)",
|
||||
},
|
||||
animation: {
|
||||
fadeIn: "fadeIn 0.25s ease-in-out",
|
||||
fadeOut: "fadeOut 0.25s ease-in-out",
|
||||
},
|
||||
keyframes: () => ({
|
||||
fadeIn: {
|
||||
"0%": { opacity: "0" },
|
||||
"100%": { opacity: "1" },
|
||||
},
|
||||
fadeOut: {
|
||||
"0%": { opacity: "1" },
|
||||
"100%": { opacity: "0" },
|
||||
},
|
||||
}),
|
||||
},
|
||||
screens: {
|
||||
tb: "640px",
|
||||
|
||||
Reference in New Issue
Block a user