mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Add registration and data caching
This commit is contained in:
@@ -1,13 +1,14 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LoginForm,
|
LoginFormType,
|
||||||
loginFormFieldNames,
|
loginFormFieldNames,
|
||||||
loginFormSchema,
|
loginFormSchema,
|
||||||
} from "@/entities/user";
|
} from "@/entities/user";
|
||||||
import { UserService } from "@/entities/user/user";
|
import { UserService } from "@/entities/user/user";
|
||||||
import { Modal } from "@/shared/ui";
|
import { Modal } from "@/shared/ui";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
@@ -16,15 +17,17 @@ export default function Login() {
|
|||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
|
setError,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<LoginForm>({ resolver: zodResolver(loginFormSchema) });
|
} = useForm<LoginFormType>({ resolver: zodResolver(loginFormSchema) });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LoginForm> = async (data) => {
|
const onSubmit: SubmitHandler<LoginFormType> = async (data) => {
|
||||||
const userInfo = await UserService.Login(data);
|
const userInfo = await UserService.Login(data);
|
||||||
mutate("user", userInfo);
|
mutate("user", userInfo);
|
||||||
router.back();
|
if (userInfo) router.back();
|
||||||
|
else setError("root", { message: "Неверный логин или пароль" });
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,36 +38,46 @@ export default function Login() {
|
|||||||
className="flex flex-col items-center justify-evenly"
|
className="flex flex-col items-center justify-evenly"
|
||||||
>
|
>
|
||||||
<h2 className="pb-4 text-4xl">.Torrent</h2>
|
<h2 className="pb-4 text-4xl">.Torrent</h2>
|
||||||
{(["username", "password"] as (keyof LoginForm)[]).map((field) => (
|
{(["username", "password"] as (keyof LoginFormType)[]).map(
|
||||||
<label
|
(field) => (
|
||||||
className="flex flex-col items-start relative w-64 py-1"
|
<label
|
||||||
key={field}
|
className="flex flex-col items-start relative w-64 py-1"
|
||||||
>
|
key={field}
|
||||||
<input
|
>
|
||||||
{...register(field)}
|
<input
|
||||||
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
{...register(field)}
|
||||||
placeholder=" "
|
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
||||||
autoComplete="off"
|
placeholder=" "
|
||||||
/>
|
autoComplete="off"
|
||||||
<span
|
/>
|
||||||
className="peer-focus/search:opacity-0
|
<span
|
||||||
|
className="peer-focus/search:opacity-0
|
||||||
peer-[:not(:placeholder-shown)]/search:opacity-0
|
peer-[:not(:placeholder-shown)]/search:opacity-0
|
||||||
transition-opacity h-0 flex items-center relative bottom-5 left-4
|
transition-opacity h-0 flex items-center relative bottom-5 left-4
|
||||||
text-lg"
|
text-lg"
|
||||||
>
|
>
|
||||||
{loginFormFieldNames[field]}
|
{loginFormFieldNames[field]}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-sm text-err w-full text-center">
|
<p className="text-sm text-err w-full text-center">
|
||||||
{errors[field]?.message}
|
{errors[field]?.message}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
))}
|
)
|
||||||
|
)}
|
||||||
|
{errors.root && (
|
||||||
|
<p className="text-sm text-err w-full text-center">
|
||||||
|
{errors.root.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Войти"
|
value="Войти"
|
||||||
className="bg-ac0 mt-2 p-1 px-4 rounded-lg"
|
className="bg-ac0 mt-2 p-1 px-4 rounded-lg"
|
||||||
/>
|
/>
|
||||||
|
<Link href="/registration" replace={true} className="text-xs pt-2">
|
||||||
|
Или <span className="hover:underline">создать аккаунт</span>
|
||||||
|
</Link>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|||||||
88
src/app/@auth/(.)registration/page.tsx
Normal file
88
src/app/@auth/(.)registration/page.tsx
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
RegistrationFormType,
|
||||||
|
registrationFormFieldNames,
|
||||||
|
RegistrationFormFields,
|
||||||
|
registrationFormSchema,
|
||||||
|
} from "@/entities/user";
|
||||||
|
import { UserService } from "@/entities/user/user";
|
||||||
|
import { Modal } from "@/shared/ui";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { mutate } from "swr";
|
||||||
|
|
||||||
|
export default function Registration() {
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setError,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<RegistrationFormType>({
|
||||||
|
resolver: zodResolver(registrationFormSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
const onSubmit: SubmitHandler<RegistrationFormType> = async (data) => {
|
||||||
|
const userInfo = await UserService.Registration(data);
|
||||||
|
mutate("user", userInfo);
|
||||||
|
if (userInfo) router.back();
|
||||||
|
else setError("root", { message: "Некорректные данные" });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal>
|
||||||
|
<div className="">
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="flex flex-col items-center justify-evenly"
|
||||||
|
>
|
||||||
|
<h2 className="pb-4 text-4xl">.Torrent</h2>
|
||||||
|
{(
|
||||||
|
Object.keys(registrationFormFieldNames) as RegistrationFormFields[]
|
||||||
|
).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"
|
||||||
|
>
|
||||||
|
{registrationFormFieldNames[field]}
|
||||||
|
</span>
|
||||||
|
<p className="text-sm text-err w-full text-center">
|
||||||
|
{errors[field]?.message}
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
))}
|
||||||
|
{errors.root && (
|
||||||
|
<p className="text-sm text-err w-full text-center">
|
||||||
|
{errors.root.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<input
|
||||||
|
type="submit"
|
||||||
|
value="Зарегестрироваться"
|
||||||
|
className="bg-ac0 mt-2 p-1 px-4 rounded-lg"
|
||||||
|
/>
|
||||||
|
<Link href="/login" replace={true} className="text-xs pt-2">
|
||||||
|
Или <span className="hover:underline">войти</span>
|
||||||
|
</Link>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -3,6 +3,21 @@ import { ItemCard } from "@/features/itemCard";
|
|||||||
import { ItemInfo } from "@/widgets/itemInfo";
|
import { ItemInfo } from "@/widgets/itemInfo";
|
||||||
import { Section } from "@/widgets/section";
|
import { Section } from "@/widgets/section";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params: { section },
|
||||||
|
}: {
|
||||||
|
params: { section: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
if (!isSection(section)) {
|
||||||
|
redirect("/");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: `.Torrent: ${ItemService.itemSections[section].addItemText}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function AddItem({
|
export default async function AddItem({
|
||||||
params: { section },
|
params: { section },
|
||||||
|
|||||||
@@ -2,11 +2,22 @@ import { isSection, ItemService, MovieService } from "@/entities/item";
|
|||||||
import { ItemCard } from "@/features/itemCard";
|
import { ItemCard } from "@/features/itemCard";
|
||||||
import { Section } from "@/widgets/section";
|
import { Section } from "@/widgets/section";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
//export const metadata: Metadata = {
|
export async function generateMetadata({
|
||||||
// title: ".Torrent: Фильмы",
|
params: { section },
|
||||||
// description: ".Torrent: Фильмы - каталог .torrent файлов для обмена фильмами",
|
}: {
|
||||||
//};
|
params: { section: string };
|
||||||
|
}): Promise<Metadata> {
|
||||||
|
if (!isSection(section)) {
|
||||||
|
redirect("/");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
title: `.Torrent: ${ItemService.itemSections[section].sectionName}`,
|
||||||
|
description: `.Torrent: ${ItemService.itemSections[section].sectionName} - ${ItemService.itemSections[section].sectionName}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default async function SectionPage({
|
export default async function SectionPage({
|
||||||
params: { section },
|
params: { section },
|
||||||
|
|||||||
@@ -7,36 +7,27 @@ export abstract class FilesService {
|
|||||||
public static async UploadCover(cover: File) {
|
public static async UploadCover(cover: File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("cover", cover);
|
formData.append("cover", cover);
|
||||||
return await HTTPService.post(
|
return await HTTPService.post(`/files/cover`, coverNameSchema, {
|
||||||
`/files/cover`,
|
body: formData,
|
||||||
coverNameSchema,
|
stringify: false,
|
||||||
formData,
|
});
|
||||||
{},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async UploadTorrent(torrent: File) {
|
public static async UploadTorrent(torrent: File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("torrent", torrent);
|
formData.append("torrent", torrent);
|
||||||
return await HTTPService.post(
|
return await HTTPService.post(`/files/torrent`, torrentNameSchema, {
|
||||||
`/files/torrent`,
|
body: formData,
|
||||||
torrentNameSchema,
|
stringify: false,
|
||||||
formData,
|
});
|
||||||
{},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async UploadFragment(fragment: File) {
|
public static async UploadFragment(fragment: File) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("fragment", fragment);
|
formData.append("fragment", fragment);
|
||||||
return await HTTPService.post(
|
return await HTTPService.post(`/files/audio`, fragmentNameSchema, {
|
||||||
`/files/audio`,
|
body: formData,
|
||||||
fragmentNameSchema,
|
stringify: false,
|
||||||
formData,
|
});
|
||||||
{},
|
|
||||||
false
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService, RequestCacheOptions } from "@/shared/utils/http";
|
||||||
import { audiobookCardsSchema } from "./schemas/audiobookCard";
|
import { audiobookCardsSchema } from "./schemas/audiobookCard";
|
||||||
import {
|
import {
|
||||||
AudiobookCreateType,
|
AudiobookCreateType,
|
||||||
@@ -17,17 +17,40 @@ import { ItemService } from "../item";
|
|||||||
|
|
||||||
@staticImplements<IItemService>()
|
@staticImplements<IItemService>()
|
||||||
export abstract class AudiobookService {
|
export abstract class AudiobookService {
|
||||||
|
public static cacheTag = "all_audiobooks";
|
||||||
|
public static urlPrefix = "audiobooks";
|
||||||
|
private static cacheOptions(custom_tag?: string): RequestCacheOptions {
|
||||||
|
return {
|
||||||
|
next: {
|
||||||
|
tags: custom_tag ? [this.cacheTag, custom_tag] : [this.cacheTag],
|
||||||
|
revalidate: 60 * 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static async GetCards() {
|
public static async GetCards() {
|
||||||
return await HTTPService.get("/audiobooks/cards", audiobookCardsSchema);
|
return await HTTPService.get(
|
||||||
|
`/${this.urlPrefix}/cards`,
|
||||||
|
audiobookCardsSchema,
|
||||||
|
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public static async Get(id: number) {
|
public static async Get(id: number) {
|
||||||
return await HTTPService.get(`/audiobooks/${id}`, audiobookSchema);
|
return await HTTPService.get(
|
||||||
|
`/${this.urlPrefix}/${id}`,
|
||||||
|
audiobookSchema,
|
||||||
|
this.cacheOptions(`/${this.urlPrefix}/${id}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public static async Add(info: AudiobookCreateType) {
|
public static async Add(info: AudiobookCreateType) {
|
||||||
return await HTTPService.post(`/audiobooks`, audiobookSchema, info);
|
return await HTTPService.post(`/${this.urlPrefix}`, audiobookSchema, {
|
||||||
|
body: info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public static async Change(id: number, info: AudiobookCreateType) {
|
public static async Change(id: number, info: AudiobookCreateType) {
|
||||||
return await HTTPService.put(`/audiobooks/${id}`, audiobookSchema, info);
|
return await HTTPService.put(`/${this.urlPrefix}/${id}`, audiobookSchema, {
|
||||||
|
body: info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetEmpty(): AudiobookCreateType {
|
public static GetEmpty(): AudiobookCreateType {
|
||||||
|
|||||||
@@ -44,14 +44,14 @@ export const audiobookSchema = audiobookBaseSchema.merge(
|
|||||||
);
|
);
|
||||||
export type AudiobookType = z.infer<typeof audiobookSchema>;
|
export type AudiobookType = z.infer<typeof audiobookSchema>;
|
||||||
|
|
||||||
export const isAudiobook = (a: any): a is AudiobookType => {
|
export const isAudiobookStrict = (a: any): a is AudiobookType => {
|
||||||
return audiobookSchema.safeParse(a).success;
|
return audiobookSchema.safeParse(a).success;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const audiobooksSchema = z.array(z.any()).transform((a) => {
|
export const audiobooksSchema = z.array(z.any()).transform((a) => {
|
||||||
const audiobooks: AudiobookType[] = [];
|
const audiobooks: AudiobookType[] = [];
|
||||||
a.forEach((e) => {
|
a.forEach((e) => {
|
||||||
if (isAudiobook(e)) audiobooks.push(audiobookSchema.parse(e));
|
if (isAudiobookStrict(e)) audiobooks.push(audiobookSchema.parse(e));
|
||||||
else console.error("Audiobook parse error - ", e);
|
else console.error("Audiobook parse error - ", e);
|
||||||
});
|
});
|
||||||
return audiobooks;
|
return audiobooks;
|
||||||
|
|||||||
@@ -36,8 +36,5 @@ export const audiobookCardsSchema = z.array(z.any()).transform((a) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const isAudiobook = (a: any): a is AudiobookCardType => {
|
export const isAudiobook = (a: any): a is AudiobookCardType => {
|
||||||
return (
|
return (a as AudiobookCardType).type === TypesOfItems.audiobook;
|
||||||
audiobookCardBaseSchema.safeParse(a).success &&
|
|
||||||
(a as AudiobookCardType).type === TypesOfItems.audiobook
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService, RequestCacheOptions } from "@/shared/utils/http";
|
||||||
import { gameCardsSchema } from "./schemas/gameCard";
|
import { gameCardsSchema } from "./schemas/gameCard";
|
||||||
import { GameCreateType, gameSchema, GameType } from "./schemas/game";
|
import { GameCreateType, gameSchema, GameType } from "./schemas/game";
|
||||||
import {
|
import {
|
||||||
@@ -13,17 +13,40 @@ import { ItemService } from "../item";
|
|||||||
|
|
||||||
@staticImplements<IItemService>()
|
@staticImplements<IItemService>()
|
||||||
export abstract class GameService {
|
export abstract class GameService {
|
||||||
|
public static cacheTag = "all_games";
|
||||||
|
public static urlPrefix = "games";
|
||||||
|
private static cacheOptions(custom_tag?: string): RequestCacheOptions {
|
||||||
|
return {
|
||||||
|
next: {
|
||||||
|
tags: custom_tag ? [this.cacheTag, custom_tag] : [this.cacheTag],
|
||||||
|
revalidate: 60 * 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static async GetCards() {
|
public static async GetCards() {
|
||||||
return await HTTPService.get("/games/cards", gameCardsSchema);
|
return await HTTPService.get(
|
||||||
|
`/${this.urlPrefix}/cards`,
|
||||||
|
gameCardsSchema,
|
||||||
|
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public static async Get(id: number) {
|
public static async Get(id: number) {
|
||||||
return await HTTPService.get(`/games/${id}`, gameSchema);
|
return await HTTPService.get(
|
||||||
|
`/${this.urlPrefix}/${id}`,
|
||||||
|
gameSchema,
|
||||||
|
this.cacheOptions(`/${this.urlPrefix}/${id}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public static async Add(info: GameCreateType) {
|
public static async Add(info: GameCreateType) {
|
||||||
return await HTTPService.post(`/games`, gameSchema, info);
|
return await HTTPService.post(`/${this.urlPrefix}`, gameSchema, {
|
||||||
|
body: info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public static async Change(id: number, info: GameCreateType) {
|
public static async Change(id: number, info: GameCreateType) {
|
||||||
return await HTTPService.put(`/games/${id}`, gameSchema, info);
|
return await HTTPService.put(`/${this.urlPrefix}/${id}`, gameSchema, {
|
||||||
|
body: info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetEmpty(): GameCreateType {
|
public static GetEmpty(): GameCreateType {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { gameCardBaseSchema } from "./gameCard";
|
|||||||
export const gameBaseSchema = gameCardBaseSchema.merge(
|
export const gameBaseSchema = gameCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
|
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
|
||||||
trailer: z.string().optional(),
|
trailer: z.string().optional().nullable(),
|
||||||
|
|
||||||
system: z.string().optional().nullable(),
|
system: z.string().optional().nullable(),
|
||||||
processor: z.string().optional().nullable(),
|
processor: z.string().optional().nullable(),
|
||||||
@@ -49,14 +49,14 @@ export const gameSchema = gameBaseSchema.merge(
|
|||||||
);
|
);
|
||||||
export type GameType = z.infer<typeof gameSchema>;
|
export type GameType = z.infer<typeof gameSchema>;
|
||||||
|
|
||||||
export const isGame = (a: any): a is GameType => {
|
export const isGameStrict = (a: any): a is GameType => {
|
||||||
return gameSchema.safeParse(a).success;
|
return gameSchema.safeParse(a).success;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gamesSchema = z.array(z.any()).transform((a) => {
|
export const gamesSchema = z.array(z.any()).transform((a) => {
|
||||||
const games: GameType[] = [];
|
const games: GameType[] = [];
|
||||||
a.forEach((e) => {
|
a.forEach((e) => {
|
||||||
if (isGame(e)) games.push(gameSchema.parse(e));
|
if (isGameStrict(e)) games.push(gameSchema.parse(e));
|
||||||
else console.error("Game parse error - ", e);
|
else console.error("Game parse error - ", e);
|
||||||
});
|
});
|
||||||
return games;
|
return games;
|
||||||
|
|||||||
@@ -36,8 +36,5 @@ export const gameCardsSchema = z.array(z.any()).transform((a) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const isGame = (a: any): a is GameCardType => {
|
export const isGame = (a: any): a is GameCardType => {
|
||||||
return (
|
return (a as GameCardType).type === TypesOfItems.game;
|
||||||
gameCardBaseSchema.safeParse(a).success &&
|
|
||||||
(a as GameCardType).type === TypesOfItems.game
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,9 @@
|
|||||||
import { ZodSchema } from "zod";
|
import { ZodSchema } from "zod";
|
||||||
import { gameCreateSchema, gameSchema } from "./game/schemas/game";
|
import { gameCreateSchema } from "./game/schemas/game";
|
||||||
import { movieCreateSchema, movieSchema } from "./movie/schemas/movie";
|
import { movieCreateSchema } from "./movie/schemas/movie";
|
||||||
import { MovieService } from "./movie/movie";
|
import { MovieService } from "./movie/movie";
|
||||||
import { HTTPService } from "@/shared/utils/http";
|
|
||||||
import { GameService } from "./game/game";
|
import { GameService } from "./game/game";
|
||||||
import {
|
import { audiobookCreateSchema } from "./audiobook/schemas/audiobook";
|
||||||
audiobookCreateSchema,
|
|
||||||
audiobookSchema,
|
|
||||||
} from "./audiobook/schemas/audiobook";
|
|
||||||
import { AudiobookService } from "./audiobook/audiobook";
|
import { AudiobookService } from "./audiobook/audiobook";
|
||||||
import {
|
import {
|
||||||
IItemService,
|
IItemService,
|
||||||
@@ -19,6 +15,7 @@ import {
|
|||||||
TypesOfItems,
|
TypesOfItems,
|
||||||
UnionItemType,
|
UnionItemType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
import { EraseCacheByTag } from "@/shared/utils/http";
|
||||||
|
|
||||||
export abstract class ItemService {
|
export abstract class ItemService {
|
||||||
private static get itemsConfiguration(): {
|
private static get itemsConfiguration(): {
|
||||||
@@ -26,11 +23,7 @@ export abstract class ItemService {
|
|||||||
sectionUrl: ItemSectionsType;
|
sectionUrl: ItemSectionsType;
|
||||||
formResolver: ZodSchema;
|
formResolver: ZodSchema;
|
||||||
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
|
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
|
||||||
AddItem: (itemInfo: ItemCreateType) => Promise<ItemType | null>;
|
service: IItemService;
|
||||||
ChangeItem: (
|
|
||||||
id: number,
|
|
||||||
itemInfo: ItemCreateType
|
|
||||||
) => Promise<ItemType | null>;
|
|
||||||
};
|
};
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
@@ -38,57 +31,63 @@ export abstract class ItemService {
|
|||||||
sectionUrl: "games",
|
sectionUrl: "games",
|
||||||
formResolver: gameCreateSchema,
|
formResolver: gameCreateSchema,
|
||||||
propertiesDescription: GameService.propertiesDescription,
|
propertiesDescription: GameService.propertiesDescription,
|
||||||
AddItem: async (itemInfo) =>
|
service: GameService,
|
||||||
await HTTPService.post(`/games`, gameSchema, itemInfo),
|
|
||||||
ChangeItem: async (id: number, itemInfo) =>
|
|
||||||
await HTTPService.put(`/games/${id}`, gameSchema, itemInfo),
|
|
||||||
},
|
},
|
||||||
[TypesOfItems.movie]: {
|
[TypesOfItems.movie]: {
|
||||||
sectionUrl: "movies",
|
sectionUrl: "movies",
|
||||||
formResolver: movieCreateSchema,
|
formResolver: movieCreateSchema,
|
||||||
propertiesDescription: MovieService.propertiesDescription,
|
propertiesDescription: MovieService.propertiesDescription,
|
||||||
AddItem: async (itemInfo) =>
|
service: MovieService,
|
||||||
await HTTPService.post(`/movies`, movieSchema, itemInfo),
|
|
||||||
ChangeItem: async (id: number, itemInfo) =>
|
|
||||||
await HTTPService.put(`/movies/${id}`, movieSchema, itemInfo),
|
|
||||||
},
|
},
|
||||||
[TypesOfItems.audiobook]: {
|
[TypesOfItems.audiobook]: {
|
||||||
sectionUrl: "audiobooks",
|
sectionUrl: "audiobooks",
|
||||||
formResolver: audiobookCreateSchema,
|
formResolver: audiobookCreateSchema,
|
||||||
propertiesDescription: AudiobookService.propertiesDescription,
|
propertiesDescription: AudiobookService.propertiesDescription,
|
||||||
AddItem: async (itemInfo) =>
|
service: AudiobookService,
|
||||||
await HTTPService.post(`/audiobooks`, audiobookSchema, itemInfo),
|
|
||||||
ChangeItem: async (id: number, itemInfo) =>
|
|
||||||
await HTTPService.put(`/audiobooks/${id}`, audiobookSchema, itemInfo),
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
static get itemSections(): {
|
static get itemSections(): {
|
||||||
[k in ItemSectionsType]: {
|
[k in ItemSectionsType]: {
|
||||||
|
sectionName: string;
|
||||||
itemType: TypesOfItems;
|
itemType: TypesOfItems;
|
||||||
popularSubsectionName: string;
|
popularSubsectionName: string;
|
||||||
sectionInviteText: string;
|
sectionInviteText: string;
|
||||||
|
addItemText: string;
|
||||||
|
sectionDescription: string;
|
||||||
service: IItemService;
|
service: IItemService;
|
||||||
};
|
};
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
games: {
|
games: {
|
||||||
|
sectionName: "Игры",
|
||||||
itemType: TypesOfItems.game,
|
itemType: TypesOfItems.game,
|
||||||
popularSubsectionName: "Популярные игры",
|
popularSubsectionName: "Популярные игры",
|
||||||
sectionInviteText: 'Перейти в раздел "Игры"',
|
sectionInviteText: 'Перейти в раздел "Игры"',
|
||||||
|
addItemText: "Добавить игру",
|
||||||
|
sectionDescription:
|
||||||
|
"каталог .torrent файлов для обмена актуальными версиями популярных игр",
|
||||||
service: GameService,
|
service: GameService,
|
||||||
},
|
},
|
||||||
movies: {
|
movies: {
|
||||||
|
sectionName: "Фильмы",
|
||||||
itemType: TypesOfItems.movie,
|
itemType: TypesOfItems.movie,
|
||||||
popularSubsectionName: "Популярные фильмы",
|
popularSubsectionName: "Популярные фильмы",
|
||||||
sectionInviteText: 'Перейти в раздел "Фильмы"',
|
sectionInviteText: 'Перейти в раздел "Фильмы"',
|
||||||
|
addItemText: "Добавить фильм",
|
||||||
|
sectionDescription:
|
||||||
|
"каталог .torrent файлов для обмена популярными фильмами в лучшем качестве",
|
||||||
service: MovieService,
|
service: MovieService,
|
||||||
},
|
},
|
||||||
audiobooks: {
|
audiobooks: {
|
||||||
|
sectionName: "Аудиокниги",
|
||||||
itemType: TypesOfItems.audiobook,
|
itemType: TypesOfItems.audiobook,
|
||||||
popularSubsectionName: "Популярные аудиокниги",
|
popularSubsectionName: "Популярные аудиокниги",
|
||||||
sectionInviteText: 'Перейти в раздел "Аудиокниги"',
|
sectionInviteText: 'Перейти в раздел "Аудиокниги"',
|
||||||
|
addItemText: "Добавить аудиокнигу",
|
||||||
|
sectionDescription:
|
||||||
|
"каталог .torrent файлов для обмена популярными аудиокнигами любимых авторов",
|
||||||
service: AudiobookService,
|
service: AudiobookService,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -120,12 +119,29 @@ export abstract class ItemService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public static async AddItem(itemInfo: ItemCreateType) {
|
public static async AddItem(itemInfo: ItemCreateType) {
|
||||||
return await this.itemsConfiguration[itemInfo.type].AddItem(itemInfo);
|
const item = await this.itemsConfiguration[itemInfo.type].service.Add(
|
||||||
|
itemInfo
|
||||||
|
);
|
||||||
|
|
||||||
|
if (item)
|
||||||
|
EraseCacheByTag(
|
||||||
|
`/${this.itemsConfiguration[itemInfo.type].service.urlPrefix}/${
|
||||||
|
item.id
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
public static async ChangeItem(id: number, itemInfo: ItemCreateType) {
|
public static async ChangeItem(id: number, itemInfo: ItemCreateType) {
|
||||||
return await this.itemsConfiguration[itemInfo.type].ChangeItem(
|
const item = await this.itemsConfiguration[itemInfo.type].service.Change(
|
||||||
id,
|
id,
|
||||||
itemInfo
|
itemInfo
|
||||||
);
|
);
|
||||||
|
if (item)
|
||||||
|
EraseCacheByTag(
|
||||||
|
`/${this.itemsConfiguration[itemInfo.type].service.urlPrefix}/${
|
||||||
|
item.id
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService, RequestCacheOptions } from "@/shared/utils/http";
|
||||||
import { movieCardsSchema } from "./schemas/movieCard";
|
import { movieCardsSchema } from "./schemas/movieCard";
|
||||||
import { MovieCreateType, movieSchema, MovieType } from "./schemas/movie";
|
import { MovieCreateType, movieSchema, MovieType } from "./schemas/movie";
|
||||||
import {
|
import {
|
||||||
@@ -13,17 +13,40 @@ import { ItemService } from "../item";
|
|||||||
|
|
||||||
@staticImplements<IItemService>()
|
@staticImplements<IItemService>()
|
||||||
export abstract class MovieService {
|
export abstract class MovieService {
|
||||||
|
public static cacheTag = "all_movies";
|
||||||
|
public static urlPrefix = "movies";
|
||||||
|
private static cacheOptions(custom_tag?: string): RequestCacheOptions {
|
||||||
|
return {
|
||||||
|
next: {
|
||||||
|
tags: custom_tag ? [this.cacheTag, custom_tag] : [this.cacheTag],
|
||||||
|
revalidate: 60 * 5,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
public static async GetCards() {
|
public static async GetCards() {
|
||||||
return await HTTPService.get("/movies/cards", movieCardsSchema);
|
return await HTTPService.get(
|
||||||
|
`/${this.urlPrefix}/cards`,
|
||||||
|
movieCardsSchema,
|
||||||
|
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public static async Get(id: number) {
|
public static async Get(id: number) {
|
||||||
return await HTTPService.get(`/movies/${id}`, movieSchema);
|
return await HTTPService.get(
|
||||||
|
`/${this.urlPrefix}/${id}`,
|
||||||
|
movieSchema,
|
||||||
|
this.cacheOptions(`/${this.urlPrefix}/${id}`)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
public static async Add(info: MovieCreateType) {
|
public static async Add(info: MovieCreateType) {
|
||||||
return await HTTPService.post(`/movies`, movieSchema, info);
|
return await HTTPService.post(`/${this.urlPrefix}`, movieSchema, {
|
||||||
|
body: info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
public static async Change(id: number, info: MovieCreateType) {
|
public static async Change(id: number, info: MovieCreateType) {
|
||||||
return await HTTPService.put(`/movies/${id}`, movieSchema, info);
|
return await HTTPService.put(`/${this.urlPrefix}/${id}`, movieSchema, {
|
||||||
|
body: info,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetEmpty(): MovieCreateType {
|
public static GetEmpty(): MovieCreateType {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { movieCardBaseSchema } from "./movieCard";
|
|||||||
export const movieBaseSchema = movieCardBaseSchema.merge(
|
export const movieBaseSchema = movieCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
|
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
|
||||||
trailer: z.string().optional(),
|
trailer: z.string().optional().nullable(),
|
||||||
|
|
||||||
language: z.string().optional().nullable(),
|
language: z.string().optional().nullable(),
|
||||||
subtitles: z.string().optional().nullable(),
|
subtitles: z.string().optional().nullable(),
|
||||||
@@ -46,14 +46,14 @@ export const movieSchema = movieBaseSchema.merge(
|
|||||||
);
|
);
|
||||||
export type MovieType = z.infer<typeof movieSchema>;
|
export type MovieType = z.infer<typeof movieSchema>;
|
||||||
|
|
||||||
export const isMovie = (a: any): a is MovieType => {
|
export const isMovieStrict = (a: any): a is MovieType => {
|
||||||
return movieSchema.safeParse(a).success;
|
return movieSchema.safeParse(a).success;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const moviesSchema = z.array(z.any()).transform((a) => {
|
export const moviesSchema = z.array(z.any()).transform((a) => {
|
||||||
const games: MovieType[] = [];
|
const games: MovieType[] = [];
|
||||||
a.forEach((e) => {
|
a.forEach((e) => {
|
||||||
if (isMovie(e)) games.push(movieSchema.parse(e));
|
if (isMovieStrict(e)) games.push(movieSchema.parse(e));
|
||||||
else console.error("Movie parse error - ", e);
|
else console.error("Movie parse error - ", e);
|
||||||
});
|
});
|
||||||
return games;
|
return games;
|
||||||
|
|||||||
@@ -36,8 +36,5 @@ export const movieCardsSchema = z.array(z.any()).transform((a) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export const isMovie = (a: any): a is MovieCardType => {
|
export const isMovie = (a: any): a is MovieCardType => {
|
||||||
return (
|
return (a as MovieCardType).type === TypesOfItems.movie;
|
||||||
movieCardBaseSchema.safeParse(a).success &&
|
|
||||||
(a as MovieCardType).type === TypesOfItems.movie
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -49,6 +49,8 @@ export type ItemPropertiesDescriptionType<T extends ItemType | ItemCreateType> =
|
|||||||
}[][];
|
}[][];
|
||||||
|
|
||||||
export interface IItemService {
|
export interface IItemService {
|
||||||
|
cacheTag: string;
|
||||||
|
urlPrefix: string;
|
||||||
GetCards(): Promise<ItemCardType[] | null>;
|
GetCards(): Promise<ItemCardType[] | null>;
|
||||||
Get(id: number): Promise<ItemType | null>;
|
Get(id: number): Promise<ItemType | null>;
|
||||||
Add(info: ItemCreateType): Promise<ItemType | null>;
|
Add(info: ItemCreateType): Promise<ItemType | null>;
|
||||||
|
|||||||
@@ -1,16 +1,28 @@
|
|||||||
import {
|
import {
|
||||||
loginFormSchema,
|
loginFormSchema,
|
||||||
loginFormFieldNames,
|
loginFormFieldNames,
|
||||||
LoginForm,
|
LoginFormType,
|
||||||
} from "./schemas/auth";
|
} from "./schemas/login";
|
||||||
|
|
||||||
|
import {
|
||||||
|
registrationFormSchema,
|
||||||
|
registrationFormFieldNames,
|
||||||
|
RegistrationFormType,
|
||||||
|
RegistrationFormFields,
|
||||||
|
} from "./schemas/registration";
|
||||||
|
|
||||||
import { userSchema, User } from "./schemas/user";
|
import { userSchema, User } from "./schemas/user";
|
||||||
import { UserService } from "./user";
|
import { UserService } from "./user";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loginFormSchema,
|
loginFormSchema,
|
||||||
loginFormFieldNames,
|
loginFormFieldNames,
|
||||||
|
registrationFormSchema,
|
||||||
|
registrationFormFieldNames,
|
||||||
UserService,
|
UserService,
|
||||||
userSchema,
|
userSchema,
|
||||||
type User,
|
type User,
|
||||||
type LoginForm,
|
type LoginFormType,
|
||||||
|
type RegistrationFormType,
|
||||||
|
type RegistrationFormFields,
|
||||||
};
|
};
|
||||||
|
|||||||
13
src/entities/user/schemas/login.ts
Normal file
13
src/entities/user/schemas/login.ts
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const loginFormSchema = z.object({
|
||||||
|
username: z.string().min(3, "Логин слишком короткий"),
|
||||||
|
password: z.string().min(3, "Пароль слишком короткий"),
|
||||||
|
});
|
||||||
|
export type LoginFormType = z.infer<typeof loginFormSchema>;
|
||||||
|
export type LoginFormFieldsType = keyof LoginFormType;
|
||||||
|
|
||||||
|
export const loginFormFieldNames: { [key in LoginFormFieldsType]: string } = {
|
||||||
|
username: "Логин",
|
||||||
|
password: "Пароль",
|
||||||
|
};
|
||||||
20
src/entities/user/schemas/registration.ts
Normal file
20
src/entities/user/schemas/registration.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const registrationFormSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(3, "Почта слишком короткий")
|
||||||
|
.email("Это не адрес электронной почты"),
|
||||||
|
name: z.string().min(3, "Логин слишком короткий"),
|
||||||
|
password: z.string().min(3, "Пароль слишком короткий"),
|
||||||
|
});
|
||||||
|
export type RegistrationFormType = z.infer<typeof registrationFormSchema>;
|
||||||
|
export type RegistrationFormFields = keyof RegistrationFormType;
|
||||||
|
|
||||||
|
export const registrationFormFieldNames: {
|
||||||
|
[key in RegistrationFormFields]: string;
|
||||||
|
} = {
|
||||||
|
email: "E-mail",
|
||||||
|
name: "Логин",
|
||||||
|
password: "Пароль",
|
||||||
|
};
|
||||||
@@ -1,16 +1,6 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { userSchema } from "./user";
|
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
|
export const tokenResponseSchema = z
|
||||||
.object({
|
.object({
|
||||||
access_token: z.string(),
|
access_token: z.string(),
|
||||||
@@ -1,35 +1,45 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
import {
|
import { LoginFormType } from "./schemas/login";
|
||||||
LoginForm,
|
|
||||||
TokenData,
|
|
||||||
tokenDataSchema,
|
|
||||||
TokenResponse,
|
|
||||||
tokenResponseSchema,
|
|
||||||
} from "./schemas/auth";
|
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import Cookies from "js-cookie";
|
import Cookies from "js-cookie";
|
||||||
|
import {
|
||||||
|
TokenData,
|
||||||
|
tokenDataSchema,
|
||||||
|
tokenResponseSchema,
|
||||||
|
} from "./schemas/token";
|
||||||
|
import { RegistrationFormType } from "./schemas/registration";
|
||||||
|
|
||||||
export abstract class UserService {
|
export abstract class UserService {
|
||||||
public static async Login(loginForm: LoginForm) {
|
public static async Registration(registrationForm: RegistrationFormType) {
|
||||||
const accessToken = await HTTPService.post(
|
const accessToken = await HTTPService.post(
|
||||||
"/auth",
|
"/auth/registration",
|
||||||
tokenResponseSchema,
|
tokenResponseSchema,
|
||||||
new URLSearchParams(Object.entries(loginForm)),
|
{ body: registrationForm }
|
||||||
{
|
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
|
||||||
},
|
|
||||||
false
|
|
||||||
);
|
);
|
||||||
if (accessToken) {
|
return this.ProcessToken(accessToken);
|
||||||
const tokenData = this.DecodeToken(accessToken);
|
}
|
||||||
|
|
||||||
|
public static async Login(loginForm: LoginFormType) {
|
||||||
|
const accessToken = await HTTPService.post("/auth", tokenResponseSchema, {
|
||||||
|
body: new URLSearchParams(Object.entries(loginForm)),
|
||||||
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||||
|
stringify: false,
|
||||||
|
});
|
||||||
|
return this.ProcessToken(accessToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ProcessToken(token: string | null) {
|
||||||
|
if (token) {
|
||||||
|
const tokenData = this.DecodeToken(token);
|
||||||
if (tokenData) {
|
if (tokenData) {
|
||||||
Cookies.set("access-token", accessToken, {
|
Cookies.set("access-token", token, {
|
||||||
secure: true,
|
secure: true,
|
||||||
expires: tokenData.expire,
|
expires: tokenData.expire,
|
||||||
});
|
});
|
||||||
return tokenData;
|
return tokenData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetToken(): string | undefined {
|
public static GetToken(): string | undefined {
|
||||||
|
|||||||
@@ -3,10 +3,11 @@
|
|||||||
import { UserService } from "@/entities/user";
|
import { UserService } from "@/entities/user";
|
||||||
import { PersonIcon } from "@/shared/assets/icons";
|
import { PersonIcon } from "@/shared/assets/icons";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import useSWR from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
import Cookies from "js-cookie";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TokenData } from "@/entities/user/schemas/auth";
|
import { ItemService } from "@/entities/item";
|
||||||
|
|
||||||
export const UserActivities = () => {
|
export const UserActivities = () => {
|
||||||
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
||||||
@@ -38,13 +39,22 @@ export const UserActivities = () => {
|
|||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
group: "Добавить:",
|
group: "Добавить:",
|
||||||
items: [
|
items: Object.entries(ItemService.itemSections).map(
|
||||||
{ name: "Добавить игру", link: "/games/add" },
|
([sectionId, section]) => {
|
||||||
{ name: "Добавить фильм", link: "/films/add" },
|
return {
|
||||||
{ name: "Добавить аудиокнигу", link: "/audiobooks/add" },
|
name: section.addItemText,
|
||||||
],
|
link: `/${sectionId}/add`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Выйти",
|
||||||
|
onClick: () => {
|
||||||
|
Cookies.remove("access-token");
|
||||||
|
mutate("user", undefined);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
{ name: "Выйти", link: "/logout" },
|
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<ul key={item.group ?? item.name}>
|
<ul key={item.group ?? item.name}>
|
||||||
{item.group && (
|
{item.group && (
|
||||||
@@ -65,14 +75,14 @@ export const UserActivities = () => {
|
|||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!item.group && item.link && (
|
{!item.group && (
|
||||||
<Link
|
<span
|
||||||
key={item.name}
|
key={item.name}
|
||||||
className="text-xl font-bold py-2 cursor-pointer hover:underline"
|
className="text-xl font-bold py-2 cursor-pointer hover:underline"
|
||||||
href={item.link}
|
onClick={item.onClick}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</span>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,30 +1,43 @@
|
|||||||
import { UserService } from "@/entities/user";
|
import { UserService } from "@/entities/user";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
type Body = BodyInit | object;
|
export type RequestCacheOptions = {
|
||||||
|
cache?: RequestCache;
|
||||||
|
next?: NextFetchRequestConfig;
|
||||||
|
};
|
||||||
|
|
||||||
|
type GetRequestOptions = RequestCacheOptions & {
|
||||||
|
headers?: HeadersInit;
|
||||||
|
};
|
||||||
|
|
||||||
|
type RequestOptions = GetRequestOptions & {
|
||||||
|
body?: BodyInit | object;
|
||||||
|
stringify?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
export abstract class HTTPService {
|
export abstract class HTTPService {
|
||||||
public static async request<Z extends z.ZodTypeAny>(
|
public static async request<Z extends z.ZodTypeAny>(
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
body?: Body,
|
options?: RequestOptions
|
||||||
headers?: HeadersInit,
|
|
||||||
stringify?: boolean
|
|
||||||
) {
|
) {
|
||||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
...((stringify ?? true) != true
|
...((options?.stringify ?? true) != true
|
||||||
? {}
|
? {}
|
||||||
: { "Content-Type": "application/json" }),
|
: { "Content-Type": "application/json" }),
|
||||||
Authorization: "Bearer " + UserService.GetToken(),
|
Authorization: "Bearer " + UserService.GetToken(),
|
||||||
...headers,
|
...options?.headers,
|
||||||
},
|
},
|
||||||
body:
|
body:
|
||||||
(stringify ?? true) != true ? (body as BodyInit) : JSON.stringify(body),
|
(options?.stringify ?? true) != true
|
||||||
cache: "no-cache",
|
? (options?.body as BodyInit)
|
||||||
|
: JSON.stringify(options?.body),
|
||||||
|
cache: options?.cache ?? options?.next ? undefined : "no-cache",
|
||||||
|
next: options?.next ?? {},
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
if (r && r.ok) return r;
|
if (r && r.ok) return r;
|
||||||
@@ -42,27 +55,27 @@ export abstract class HTTPService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async get<Z extends z.ZodTypeAny>(url: string, schema: Z) {
|
public static async get<Z extends z.ZodTypeAny>(
|
||||||
return await this.request<Z>("GET", url, schema);
|
url: string,
|
||||||
|
schema: Z,
|
||||||
|
options?: GetRequestOptions
|
||||||
|
) {
|
||||||
|
return await this.request<Z>("GET", url, schema, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async post<Z extends z.ZodTypeAny>(
|
public static async post<Z extends z.ZodTypeAny>(
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
body?: Body,
|
options?: RequestOptions
|
||||||
headers?: HeadersInit,
|
|
||||||
stringify?: boolean
|
|
||||||
) {
|
) {
|
||||||
return await this.request<Z>("POST", url, schema, body, headers, stringify);
|
return await this.request<Z>("POST", url, schema, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async put<Z extends z.ZodType>(
|
public static async put<Z extends z.ZodType>(
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
body?: Body,
|
options?: RequestOptions
|
||||||
headers?: HeadersInit,
|
|
||||||
stringify?: boolean
|
|
||||||
) {
|
) {
|
||||||
return await this.request<Z>("PUT", url, schema, body, headers, stringify);
|
return await this.request<Z>("PUT", url, schema, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5
src/shared/utils/http/index.ts
Normal file
5
src/shared/utils/http/index.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { HTTPService, type RequestCacheOptions } from "./http";
|
||||||
|
export { HTTPService, type RequestCacheOptions };
|
||||||
|
|
||||||
|
import { EraseCacheByTag, EraseCacheByTags } from "./revalidate";
|
||||||
|
export { EraseCacheByTag, EraseCacheByTags };
|
||||||
11
src/shared/utils/http/revalidate.ts
Normal file
11
src/shared/utils/http/revalidate.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { revalidateTag } from "next/cache";
|
||||||
|
|
||||||
|
export const EraseCacheByTag = (tag: string) => {
|
||||||
|
revalidateTag(tag);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const EraseCacheByTags = (tags: string[]) => {
|
||||||
|
tags.forEach((tag) => EraseCacheByTag(tag));
|
||||||
|
};
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
import { getYouTubeID } from "./getYoutubeId";
|
|
||||||
|
|
||||||
export { getYouTubeID };
|
|
||||||
@@ -7,12 +7,7 @@ import Link from "next/link";
|
|||||||
import { useSelectedLayoutSegment } from "next/navigation";
|
import { useSelectedLayoutSegment } from "next/navigation";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { UserActivities } from "@/features/userActivities";
|
import { UserActivities } from "@/features/userActivities";
|
||||||
|
import { ItemSections, ItemService } from "@/entities/item";
|
||||||
const sections = [
|
|
||||||
{ title: "Игры", href: "/games" },
|
|
||||||
{ title: "Фильмы", href: "/movies" },
|
|
||||||
{ title: "Аудиокниги", href: "/audiobooks" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const currentPageName = useSelectedLayoutSegment();
|
const currentPageName = useSelectedLayoutSegment();
|
||||||
@@ -25,21 +20,21 @@ export const Header = () => {
|
|||||||
>
|
>
|
||||||
<h1 className="text-4xl font-bold flex items-center">
|
<h1 className="text-4xl font-bold flex items-center">
|
||||||
<div className="lp:hidden">
|
<div className="lp:hidden">
|
||||||
<MobileMenu sections={sections} />
|
<MobileMenu />
|
||||||
</div>
|
</div>
|
||||||
<Link href="/">.Torrent</Link>
|
<Link href="/">.Torrent</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="hidden text-2xl lp:block">
|
<div className="hidden text-2xl lp:block">
|
||||||
{sections.map((section) => (
|
{ItemSections.map((section) => (
|
||||||
<Link
|
<Link
|
||||||
key={section.title}
|
key={section}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-5 cursor-pointer hover:underline underline-offset-2",
|
"px-5 cursor-pointer hover:underline underline-offset-2",
|
||||||
currentPageName === section.href && "underline"
|
currentPageName === section && "underline"
|
||||||
)}
|
)}
|
||||||
href={section.href}
|
href={"/" + section}
|
||||||
>
|
>
|
||||||
{section.title}
|
{ItemService.itemSections[section].sectionName}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { ItemSections, ItemService } from "@/entities/item";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = () => {
|
||||||
sections,
|
|
||||||
}: {
|
|
||||||
sections: { title: string; href: string }[];
|
|
||||||
}) => {
|
|
||||||
const [open, changeMenuOpen] = useState<boolean>(false);
|
const [open, changeMenuOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -35,13 +32,13 @@ export const MobileMenu = ({
|
|||||||
)}
|
)}
|
||||||
onClick={() => changeMenuOpen(false)}
|
onClick={() => changeMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{sections.map((section) => (
|
{ItemSections.map((section) => (
|
||||||
<Link
|
<Link
|
||||||
key={section.title}
|
key={section}
|
||||||
className="text-xl py-2 cursor-pointer hover:underline"
|
className="text-xl py-2 cursor-pointer hover:underline"
|
||||||
href={section.href}
|
href={"/" + section}
|
||||||
>
|
>
|
||||||
{section.title}
|
{ItemService.itemSections[section].sectionName}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { isAudiobook, isGame, ItemCreateType, ItemType } from "@/entities/item";
|
import {
|
||||||
|
isAudiobook,
|
||||||
|
isGame,
|
||||||
|
isMovie,
|
||||||
|
ItemCreateType,
|
||||||
|
ItemType,
|
||||||
|
} from "@/entities/item";
|
||||||
import { UserService } from "@/entities/user";
|
import { UserService } from "@/entities/user";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
@@ -12,8 +18,8 @@ import { ItemProperties } from "./itemProperties";
|
|||||||
import { ItemTrailer } from "./itemTrailer";
|
import { ItemTrailer } from "./itemTrailer";
|
||||||
import { ItemTorrent } from "./itemTorrent";
|
import { ItemTorrent } from "./itemTorrent";
|
||||||
import { ItemDetails } from "./itemDetails";
|
import { ItemDetails } from "./itemDetails";
|
||||||
import { isMovie } from "@/entities/item/movie/schemas/movie";
|
|
||||||
import { ItemFragment } from "./itemFragment";
|
import { ItemFragment } from "./itemFragment";
|
||||||
|
import { EraseCacheByTag } from "@/shared/utils/http";
|
||||||
|
|
||||||
export const ItemInfo = <T extends ItemType | ItemCreateType>({
|
export const ItemInfo = <T extends ItemType | ItemCreateType>({
|
||||||
item: init_item,
|
item: init_item,
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ export const ItemTorrent = ({
|
|||||||
className="text-right text-sm relative top-4 lp:-top-4"
|
className="text-right text-sm relative top-4 lp:-top-4"
|
||||||
href="/how_to_download"
|
href="/how_to_download"
|
||||||
>
|
>
|
||||||
Как скачать игру
|
Как скачать с помощью
|
||||||
<br /> с помощью .torrent файла?
|
<br /> .torrent файла?
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||||
import { getYouTubeID } from "@/shared/utils";
|
import { getYouTubeID } from "@/shared/utils/getYoutubeId";
|
||||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
||||||
|
|
||||||
export const ItemTrailer = ({
|
export const ItemTrailer = ({
|
||||||
@@ -9,8 +9,8 @@ export const ItemTrailer = ({
|
|||||||
setFormValue: setValue,
|
setFormValue: setValue,
|
||||||
registerFormField: register,
|
registerFormField: register,
|
||||||
}: {
|
}: {
|
||||||
trailer: string | undefined;
|
trailer: string | undefined | null;
|
||||||
default_trailer: string | undefined;
|
default_trailer: string | undefined | null;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
||||||
|
|||||||
Reference in New Issue
Block a user