From 56a0841322f31c436442e06efb4ba17d4c7cde63 Mon Sep 17 00:00:00 2001 From: StepanovPlaton Date: Sat, 15 Jun 2024 18:25:29 +0400 Subject: [PATCH] Add registration and data caching --- src/app/@auth/(.)login/page.tsx | 63 +++++++------ src/app/@auth/(.)registration/page.tsx | 88 +++++++++++++++++++ src/app/[section]/add/page.tsx | 15 ++++ src/app/[section]/page.tsx | 19 +++- src/entities/files/files.ts | 33 +++---- src/entities/item/audiobook/audiobook.ts | 33 +++++-- .../item/audiobook/schemas/audiobook.ts | 4 +- .../item/audiobook/schemas/audiobookCard.ts | 5 +- src/entities/item/game/game.ts | 33 +++++-- src/entities/item/game/schemas/game.ts | 6 +- src/entities/item/game/schemas/gameCard.ts | 5 +- src/entities/item/item.ts | 68 ++++++++------ src/entities/item/movie/movie.ts | 33 +++++-- src/entities/item/movie/schemas/movie.ts | 6 +- src/entities/item/movie/schemas/movieCard.ts | 5 +- src/entities/item/types.ts | 2 + src/entities/user/index.ts | 18 +++- src/entities/user/schemas/login.ts | 13 +++ src/entities/user/schemas/registration.ts | 20 +++++ .../user/schemas/{auth.ts => token.ts} | 10 --- src/entities/user/user.ts | 44 ++++++---- .../userActivities/userActivities.tsx | 34 ++++--- src/shared/utils/{ => http}/http.ts | 49 +++++++---- src/shared/utils/http/index.ts | 5 ++ src/shared/utils/http/revalidate.ts | 11 +++ src/shared/utils/index.ts | 3 - src/widgets/header/header.tsx | 19 ++-- src/widgets/header/mobileMenu/mobileMenu.tsx | 15 ++-- src/widgets/itemInfo/itemInfo.tsx | 10 ++- src/widgets/itemInfo/itemTorrent.tsx | 4 +- src/widgets/itemInfo/itemTrailer.tsx | 6 +- 31 files changed, 477 insertions(+), 202 deletions(-) create mode 100644 src/app/@auth/(.)registration/page.tsx create mode 100644 src/entities/user/schemas/login.ts create mode 100644 src/entities/user/schemas/registration.ts rename src/entities/user/schemas/{auth.ts => token.ts} (59%) rename src/shared/utils/{ => http}/http.ts (53%) create mode 100644 src/shared/utils/http/index.ts create mode 100644 src/shared/utils/http/revalidate.ts delete mode 100644 src/shared/utils/index.ts diff --git a/src/app/@auth/(.)login/page.tsx b/src/app/@auth/(.)login/page.tsx index 2e2cb2f..39d0a21 100644 --- a/src/app/@auth/(.)login/page.tsx +++ b/src/app/@auth/(.)login/page.tsx @@ -1,13 +1,14 @@ "use client"; import { - LoginForm, + LoginFormType, loginFormFieldNames, loginFormSchema, } 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"; @@ -16,15 +17,17 @@ export default function Login() { const { register, handleSubmit, + setError, formState: { errors }, - } = useForm({ resolver: zodResolver(loginFormSchema) }); + } = useForm({ resolver: zodResolver(loginFormSchema) }); const router = useRouter(); - const onSubmit: SubmitHandler = async (data) => { + const onSubmit: SubmitHandler = async (data) => { const userInfo = await UserService.Login(data); mutate("user", userInfo); - router.back(); + if (userInfo) router.back(); + else setError("root", { message: "Неверный логин или пароль" }); }; return ( @@ -35,36 +38,46 @@ export default function Login() { className="flex flex-col items-center justify-evenly" >

.Torrent

- {(["username", "password"] as (keyof LoginForm)[]).map((field) => ( - + ) + )} + {errors.root && ( +

+ {errors.root.message} +

+ )} + + Или создать аккаунт + diff --git a/src/app/@auth/(.)registration/page.tsx b/src/app/@auth/(.)registration/page.tsx new file mode 100644 index 0000000..96ec24b --- /dev/null +++ b/src/app/@auth/(.)registration/page.tsx @@ -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({ + resolver: zodResolver(registrationFormSchema), + }); + + const router = useRouter(); + + const onSubmit: SubmitHandler = async (data) => { + const userInfo = await UserService.Registration(data); + mutate("user", userInfo); + if (userInfo) router.back(); + else setError("root", { message: "Некорректные данные" }); + }; + + return ( + +
+
+

.Torrent

+ {( + Object.keys(registrationFormFieldNames) as RegistrationFormFields[] + ).map((field) => ( + + ))} + {errors.root && ( +

+ {errors.root.message} +

+ )} + + + + Или войти + +
+
+
+ ); +} diff --git a/src/app/[section]/add/page.tsx b/src/app/[section]/add/page.tsx index 488792e..8c5422f 100644 --- a/src/app/[section]/add/page.tsx +++ b/src/app/[section]/add/page.tsx @@ -3,6 +3,21 @@ import { ItemCard } from "@/features/itemCard"; import { ItemInfo } from "@/widgets/itemInfo"; import { Section } from "@/widgets/section"; import { redirect } from "next/navigation"; +import { Metadata } from "next"; + +export async function generateMetadata({ + params: { section }, +}: { + params: { section: string }; +}): Promise { + if (!isSection(section)) { + redirect("/"); + return {}; + } + return { + title: `.Torrent: ${ItemService.itemSections[section].addItemText}`, + }; +} export default async function AddItem({ params: { section }, diff --git a/src/app/[section]/page.tsx b/src/app/[section]/page.tsx index 70a9758..29782ae 100644 --- a/src/app/[section]/page.tsx +++ b/src/app/[section]/page.tsx @@ -2,11 +2,22 @@ import { isSection, ItemService, MovieService } from "@/entities/item"; import { ItemCard } from "@/features/itemCard"; import { Section } from "@/widgets/section"; import { redirect } from "next/navigation"; +import { Metadata } from "next"; -//export const metadata: Metadata = { -// title: ".Torrent: Фильмы", -// description: ".Torrent: Фильмы - каталог .torrent файлов для обмена фильмами", -//}; +export async function generateMetadata({ + params: { section }, +}: { + params: { section: string }; +}): Promise { + 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({ params: { section }, diff --git a/src/entities/files/files.ts b/src/entities/files/files.ts index 03e0df0..29633fe 100644 --- a/src/entities/files/files.ts +++ b/src/entities/files/files.ts @@ -7,36 +7,27 @@ export abstract class FilesService { public static async UploadCover(cover: File) { const formData = new FormData(); formData.append("cover", cover); - return await HTTPService.post( - `/files/cover`, - coverNameSchema, - formData, - {}, - false - ); + return await HTTPService.post(`/files/cover`, coverNameSchema, { + body: formData, + stringify: false, + }); } public static async UploadTorrent(torrent: File) { const formData = new FormData(); formData.append("torrent", torrent); - return await HTTPService.post( - `/files/torrent`, - torrentNameSchema, - formData, - {}, - false - ); + return await HTTPService.post(`/files/torrent`, torrentNameSchema, { + body: formData, + stringify: false, + }); } public static async UploadFragment(fragment: File) { const formData = new FormData(); formData.append("fragment", fragment); - return await HTTPService.post( - `/files/audio`, - fragmentNameSchema, - formData, - {}, - false - ); + return await HTTPService.post(`/files/audio`, fragmentNameSchema, { + body: formData, + stringify: false, + }); } } diff --git a/src/entities/item/audiobook/audiobook.ts b/src/entities/item/audiobook/audiobook.ts index 1698c2d..abe0a4f 100644 --- a/src/entities/item/audiobook/audiobook.ts +++ b/src/entities/item/audiobook/audiobook.ts @@ -1,4 +1,4 @@ -import { HTTPService } from "@/shared/utils/http"; +import { HTTPService, RequestCacheOptions } from "@/shared/utils/http"; import { audiobookCardsSchema } from "./schemas/audiobookCard"; import { AudiobookCreateType, @@ -17,17 +17,40 @@ import { ItemService } from "../item"; @staticImplements() 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() { - 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) { - 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) { - return await HTTPService.post(`/audiobooks`, audiobookSchema, info); + return await HTTPService.post(`/${this.urlPrefix}`, audiobookSchema, { + body: info, + }); } 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 { diff --git a/src/entities/item/audiobook/schemas/audiobook.ts b/src/entities/item/audiobook/schemas/audiobook.ts index b41bb60..8f241e2 100644 --- a/src/entities/item/audiobook/schemas/audiobook.ts +++ b/src/entities/item/audiobook/schemas/audiobook.ts @@ -44,14 +44,14 @@ export const audiobookSchema = audiobookBaseSchema.merge( ); export type AudiobookType = z.infer; -export const isAudiobook = (a: any): a is AudiobookType => { +export const isAudiobookStrict = (a: any): a is AudiobookType => { return audiobookSchema.safeParse(a).success; }; export const audiobooksSchema = z.array(z.any()).transform((a) => { const audiobooks: AudiobookType[] = []; 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); }); return audiobooks; diff --git a/src/entities/item/audiobook/schemas/audiobookCard.ts b/src/entities/item/audiobook/schemas/audiobookCard.ts index e4512f2..dfcc32b 100644 --- a/src/entities/item/audiobook/schemas/audiobookCard.ts +++ b/src/entities/item/audiobook/schemas/audiobookCard.ts @@ -36,8 +36,5 @@ export const audiobookCardsSchema = z.array(z.any()).transform((a) => { }); export const isAudiobook = (a: any): a is AudiobookCardType => { - return ( - audiobookCardBaseSchema.safeParse(a).success && - (a as AudiobookCardType).type === TypesOfItems.audiobook - ); + return (a as AudiobookCardType).type === TypesOfItems.audiobook; }; diff --git a/src/entities/item/game/game.ts b/src/entities/item/game/game.ts index 24e38d9..e702939 100644 --- a/src/entities/item/game/game.ts +++ b/src/entities/item/game/game.ts @@ -1,4 +1,4 @@ -import { HTTPService } from "@/shared/utils/http"; +import { HTTPService, RequestCacheOptions } from "@/shared/utils/http"; import { gameCardsSchema } from "./schemas/gameCard"; import { GameCreateType, gameSchema, GameType } from "./schemas/game"; import { @@ -13,17 +13,40 @@ import { ItemService } from "../item"; @staticImplements() 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() { - 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) { - 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) { - return await HTTPService.post(`/games`, gameSchema, info); + return await HTTPService.post(`/${this.urlPrefix}`, gameSchema, { + body: info, + }); } 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 { diff --git a/src/entities/item/game/schemas/game.ts b/src/entities/item/game/schemas/game.ts index c0fb88a..7983111 100644 --- a/src/entities/item/game/schemas/game.ts +++ b/src/entities/item/game/schemas/game.ts @@ -4,7 +4,7 @@ import { gameCardBaseSchema } from "./gameCard"; export const gameBaseSchema = gameCardBaseSchema.merge( z.object({ torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"), - trailer: z.string().optional(), + trailer: z.string().optional().nullable(), system: z.string().optional().nullable(), processor: z.string().optional().nullable(), @@ -49,14 +49,14 @@ export const gameSchema = gameBaseSchema.merge( ); export type GameType = z.infer; -export const isGame = (a: any): a is GameType => { +export const isGameStrict = (a: any): a is GameType => { return gameSchema.safeParse(a).success; }; export const gamesSchema = z.array(z.any()).transform((a) => { const games: GameType[] = []; 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); }); return games; diff --git a/src/entities/item/game/schemas/gameCard.ts b/src/entities/item/game/schemas/gameCard.ts index caa1765..8c99a65 100644 --- a/src/entities/item/game/schemas/gameCard.ts +++ b/src/entities/item/game/schemas/gameCard.ts @@ -36,8 +36,5 @@ export const gameCardsSchema = z.array(z.any()).transform((a) => { }); export const isGame = (a: any): a is GameCardType => { - return ( - gameCardBaseSchema.safeParse(a).success && - (a as GameCardType).type === TypesOfItems.game - ); + return (a as GameCardType).type === TypesOfItems.game; }; diff --git a/src/entities/item/item.ts b/src/entities/item/item.ts index 89349f4..655b90b 100644 --- a/src/entities/item/item.ts +++ b/src/entities/item/item.ts @@ -1,13 +1,9 @@ import { ZodSchema } from "zod"; -import { gameCreateSchema, gameSchema } from "./game/schemas/game"; -import { movieCreateSchema, movieSchema } from "./movie/schemas/movie"; +import { gameCreateSchema } from "./game/schemas/game"; +import { movieCreateSchema } from "./movie/schemas/movie"; import { MovieService } from "./movie/movie"; -import { HTTPService } from "@/shared/utils/http"; import { GameService } from "./game/game"; -import { - audiobookCreateSchema, - audiobookSchema, -} from "./audiobook/schemas/audiobook"; +import { audiobookCreateSchema } from "./audiobook/schemas/audiobook"; import { AudiobookService } from "./audiobook/audiobook"; import { IItemService, @@ -19,6 +15,7 @@ import { TypesOfItems, UnionItemType, } from "./types"; +import { EraseCacheByTag } from "@/shared/utils/http"; export abstract class ItemService { private static get itemsConfiguration(): { @@ -26,11 +23,7 @@ export abstract class ItemService { sectionUrl: ItemSectionsType; formResolver: ZodSchema; propertiesDescription: ItemPropertiesDescriptionType; - AddItem: (itemInfo: ItemCreateType) => Promise; - ChangeItem: ( - id: number, - itemInfo: ItemCreateType - ) => Promise; + service: IItemService; }; } { return { @@ -38,57 +31,63 @@ export abstract class ItemService { sectionUrl: "games", formResolver: gameCreateSchema, propertiesDescription: GameService.propertiesDescription, - AddItem: async (itemInfo) => - await HTTPService.post(`/games`, gameSchema, itemInfo), - ChangeItem: async (id: number, itemInfo) => - await HTTPService.put(`/games/${id}`, gameSchema, itemInfo), + service: GameService, }, [TypesOfItems.movie]: { sectionUrl: "movies", formResolver: movieCreateSchema, propertiesDescription: MovieService.propertiesDescription, - AddItem: async (itemInfo) => - await HTTPService.post(`/movies`, movieSchema, itemInfo), - ChangeItem: async (id: number, itemInfo) => - await HTTPService.put(`/movies/${id}`, movieSchema, itemInfo), + service: MovieService, }, [TypesOfItems.audiobook]: { sectionUrl: "audiobooks", formResolver: audiobookCreateSchema, propertiesDescription: AudiobookService.propertiesDescription, - AddItem: async (itemInfo) => - await HTTPService.post(`/audiobooks`, audiobookSchema, itemInfo), - ChangeItem: async (id: number, itemInfo) => - await HTTPService.put(`/audiobooks/${id}`, audiobookSchema, itemInfo), + service: AudiobookService, }, }; } static get itemSections(): { [k in ItemSectionsType]: { + sectionName: string; itemType: TypesOfItems; popularSubsectionName: string; sectionInviteText: string; + addItemText: string; + sectionDescription: string; service: IItemService; }; } { return { games: { + sectionName: "Игры", itemType: TypesOfItems.game, popularSubsectionName: "Популярные игры", sectionInviteText: 'Перейти в раздел "Игры"', + addItemText: "Добавить игру", + sectionDescription: + "каталог .torrent файлов для обмена актуальными версиями популярных игр", service: GameService, }, movies: { + sectionName: "Фильмы", itemType: TypesOfItems.movie, popularSubsectionName: "Популярные фильмы", sectionInviteText: 'Перейти в раздел "Фильмы"', + addItemText: "Добавить фильм", + sectionDescription: + "каталог .torrent файлов для обмена популярными фильмами в лучшем качестве", service: MovieService, }, audiobooks: { + sectionName: "Аудиокниги", itemType: TypesOfItems.audiobook, popularSubsectionName: "Популярные аудиокниги", sectionInviteText: 'Перейти в раздел "Аудиокниги"', + addItemText: "Добавить аудиокнигу", + sectionDescription: + "каталог .torrent файлов для обмена популярными аудиокнигами любимых авторов", service: AudiobookService, }, }; @@ -120,12 +119,29 @@ export abstract class ItemService { } 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) { - return await this.itemsConfiguration[itemInfo.type].ChangeItem( + const item = await this.itemsConfiguration[itemInfo.type].service.Change( id, itemInfo ); + if (item) + EraseCacheByTag( + `/${this.itemsConfiguration[itemInfo.type].service.urlPrefix}/${ + item.id + }` + ); + return item; } } diff --git a/src/entities/item/movie/movie.ts b/src/entities/item/movie/movie.ts index 9ef724b..dadfeb7 100644 --- a/src/entities/item/movie/movie.ts +++ b/src/entities/item/movie/movie.ts @@ -1,4 +1,4 @@ -import { HTTPService } from "@/shared/utils/http"; +import { HTTPService, RequestCacheOptions } from "@/shared/utils/http"; import { movieCardsSchema } from "./schemas/movieCard"; import { MovieCreateType, movieSchema, MovieType } from "./schemas/movie"; import { @@ -13,17 +13,40 @@ import { ItemService } from "../item"; @staticImplements() 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() { - 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) { - 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) { - return await HTTPService.post(`/movies`, movieSchema, info); + return await HTTPService.post(`/${this.urlPrefix}`, movieSchema, { + body: info, + }); } 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 { diff --git a/src/entities/item/movie/schemas/movie.ts b/src/entities/item/movie/schemas/movie.ts index 04fd74e..5899ce2 100644 --- a/src/entities/item/movie/schemas/movie.ts +++ b/src/entities/item/movie/schemas/movie.ts @@ -4,7 +4,7 @@ import { movieCardBaseSchema } from "./movieCard"; export const movieBaseSchema = movieCardBaseSchema.merge( z.object({ torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"), - trailer: z.string().optional(), + trailer: z.string().optional().nullable(), language: z.string().optional().nullable(), subtitles: z.string().optional().nullable(), @@ -46,14 +46,14 @@ export const movieSchema = movieBaseSchema.merge( ); export type MovieType = z.infer; -export const isMovie = (a: any): a is MovieType => { +export const isMovieStrict = (a: any): a is MovieType => { return movieSchema.safeParse(a).success; }; export const moviesSchema = z.array(z.any()).transform((a) => { const games: MovieType[] = []; 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); }); return games; diff --git a/src/entities/item/movie/schemas/movieCard.ts b/src/entities/item/movie/schemas/movieCard.ts index 042b2fd..e919113 100644 --- a/src/entities/item/movie/schemas/movieCard.ts +++ b/src/entities/item/movie/schemas/movieCard.ts @@ -36,8 +36,5 @@ export const movieCardsSchema = z.array(z.any()).transform((a) => { }); export const isMovie = (a: any): a is MovieCardType => { - return ( - movieCardBaseSchema.safeParse(a).success && - (a as MovieCardType).type === TypesOfItems.movie - ); + return (a as MovieCardType).type === TypesOfItems.movie; }; diff --git a/src/entities/item/types.ts b/src/entities/item/types.ts index d19e88a..8f11f68 100644 --- a/src/entities/item/types.ts +++ b/src/entities/item/types.ts @@ -49,6 +49,8 @@ export type ItemPropertiesDescriptionType = }[][]; export interface IItemService { + cacheTag: string; + urlPrefix: string; GetCards(): Promise; Get(id: number): Promise; Add(info: ItemCreateType): Promise; diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts index 95c97e5..d0ae340 100644 --- a/src/entities/user/index.ts +++ b/src/entities/user/index.ts @@ -1,16 +1,28 @@ import { loginFormSchema, loginFormFieldNames, - LoginForm, -} from "./schemas/auth"; + LoginFormType, +} from "./schemas/login"; + +import { + registrationFormSchema, + registrationFormFieldNames, + RegistrationFormType, + RegistrationFormFields, +} from "./schemas/registration"; + import { userSchema, User } from "./schemas/user"; import { UserService } from "./user"; export { loginFormSchema, loginFormFieldNames, + registrationFormSchema, + registrationFormFieldNames, UserService, userSchema, type User, - type LoginForm, + type LoginFormType, + type RegistrationFormType, + type RegistrationFormFields, }; diff --git a/src/entities/user/schemas/login.ts b/src/entities/user/schemas/login.ts new file mode 100644 index 0000000..d1e1363 --- /dev/null +++ b/src/entities/user/schemas/login.ts @@ -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; +export type LoginFormFieldsType = keyof LoginFormType; + +export const loginFormFieldNames: { [key in LoginFormFieldsType]: string } = { + username: "Логин", + password: "Пароль", +}; diff --git a/src/entities/user/schemas/registration.ts b/src/entities/user/schemas/registration.ts new file mode 100644 index 0000000..0151142 --- /dev/null +++ b/src/entities/user/schemas/registration.ts @@ -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; +export type RegistrationFormFields = keyof RegistrationFormType; + +export const registrationFormFieldNames: { + [key in RegistrationFormFields]: string; +} = { + email: "E-mail", + name: "Логин", + password: "Пароль", +}; diff --git a/src/entities/user/schemas/auth.ts b/src/entities/user/schemas/token.ts similarity index 59% rename from src/entities/user/schemas/auth.ts rename to src/entities/user/schemas/token.ts index 1ef06e0..9425416 100644 --- a/src/entities/user/schemas/auth.ts +++ b/src/entities/user/schemas/token.ts @@ -1,16 +1,6 @@ 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; - export const tokenResponseSchema = z .object({ access_token: z.string(), diff --git a/src/entities/user/user.ts b/src/entities/user/user.ts index ba33d33..3a4a9ad 100644 --- a/src/entities/user/user.ts +++ b/src/entities/user/user.ts @@ -1,35 +1,45 @@ import { HTTPService } from "@/shared/utils/http"; -import { - LoginForm, - TokenData, - tokenDataSchema, - TokenResponse, - tokenResponseSchema, -} from "./schemas/auth"; +import { LoginFormType } from "./schemas/login"; import { jwtDecode } from "jwt-decode"; import Cookies from "js-cookie"; +import { + TokenData, + tokenDataSchema, + tokenResponseSchema, +} from "./schemas/token"; +import { RegistrationFormType } from "./schemas/registration"; export abstract class UserService { - public static async Login(loginForm: LoginForm) { + public static async Registration(registrationForm: RegistrationFormType) { const accessToken = await HTTPService.post( - "/auth", + "/auth/registration", tokenResponseSchema, - new URLSearchParams(Object.entries(loginForm)), - { - "Content-Type": "application/x-www-form-urlencoded", - }, - false + { body: registrationForm } ); - if (accessToken) { - const tokenData = this.DecodeToken(accessToken); + return this.ProcessToken(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) { - Cookies.set("access-token", accessToken, { + Cookies.set("access-token", token, { secure: true, expires: tokenData.expire, }); return tokenData; } } + return null; } public static GetToken(): string | undefined { diff --git a/src/features/userActivities/userActivities.tsx b/src/features/userActivities/userActivities.tsx index 0858e72..3e63022 100644 --- a/src/features/userActivities/userActivities.tsx +++ b/src/features/userActivities/userActivities.tsx @@ -3,10 +3,11 @@ import { UserService } from "@/entities/user"; import { PersonIcon } from "@/shared/assets/icons"; import Link from "next/link"; -import useSWR from "swr"; +import useSWR, { mutate } from "swr"; import clsx from "clsx"; +import Cookies from "js-cookie"; import { useState } from "react"; -import { TokenData } from "@/entities/user/schemas/auth"; +import { ItemService } from "@/entities/item"; export const UserActivities = () => { const { data: me } = useSWR("user", () => UserService.IdentifyYourself()); @@ -38,13 +39,22 @@ export const UserActivities = () => { {[ { group: "Добавить:", - items: [ - { name: "Добавить игру", link: "/games/add" }, - { name: "Добавить фильм", link: "/films/add" }, - { name: "Добавить аудиокнигу", link: "/audiobooks/add" }, - ], + items: Object.entries(ItemService.itemSections).map( + ([sectionId, section]) => { + return { + name: section.addItemText, + link: `/${sectionId}/add`, + }; + } + ), + }, + { + name: "Выйти", + onClick: () => { + Cookies.remove("access-token"); + mutate("user", undefined); + }, }, - { name: "Выйти", link: "/logout" }, ].map((item) => (
    {item.group && ( @@ -65,14 +75,14 @@ export const UserActivities = () => { )} - {!item.group && item.link && ( - {item.name} - + )}
))} diff --git a/src/shared/utils/http.ts b/src/shared/utils/http/http.ts similarity index 53% rename from src/shared/utils/http.ts rename to src/shared/utils/http/http.ts index 1e037c1..ace6c72 100644 --- a/src/shared/utils/http.ts +++ b/src/shared/utils/http/http.ts @@ -1,30 +1,43 @@ import { UserService } from "@/entities/user"; 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 { public static async request( method: "GET" | "POST" | "PUT" | "DELETE", url: string, schema: Z, - body?: Body, - headers?: HeadersInit, - stringify?: boolean + options?: RequestOptions ) { return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, { method: method, headers: { accept: "application/json", - ...((stringify ?? true) != true + ...((options?.stringify ?? true) != true ? {} : { "Content-Type": "application/json" }), Authorization: "Bearer " + UserService.GetToken(), - ...headers, + ...options?.headers, }, body: - (stringify ?? true) != true ? (body as BodyInit) : JSON.stringify(body), - cache: "no-cache", + (options?.stringify ?? true) != true + ? (options?.body as BodyInit) + : JSON.stringify(options?.body), + cache: options?.cache ?? options?.next ? undefined : "no-cache", + next: options?.next ?? {}, }) .then((r) => { if (r && r.ok) return r; @@ -42,27 +55,27 @@ export abstract class HTTPService { }); } - public static async get(url: string, schema: Z) { - return await this.request("GET", url, schema); + public static async get( + url: string, + schema: Z, + options?: GetRequestOptions + ) { + return await this.request("GET", url, schema, options); } public static async post( url: string, schema: Z, - body?: Body, - headers?: HeadersInit, - stringify?: boolean + options?: RequestOptions ) { - return await this.request("POST", url, schema, body, headers, stringify); + return await this.request("POST", url, schema, options); } public static async put( url: string, schema: Z, - body?: Body, - headers?: HeadersInit, - stringify?: boolean + options?: RequestOptions ) { - return await this.request("PUT", url, schema, body, headers, stringify); + return await this.request("PUT", url, schema, options); } } diff --git a/src/shared/utils/http/index.ts b/src/shared/utils/http/index.ts new file mode 100644 index 0000000..6b2ed17 --- /dev/null +++ b/src/shared/utils/http/index.ts @@ -0,0 +1,5 @@ +import { HTTPService, type RequestCacheOptions } from "./http"; +export { HTTPService, type RequestCacheOptions }; + +import { EraseCacheByTag, EraseCacheByTags } from "./revalidate"; +export { EraseCacheByTag, EraseCacheByTags }; diff --git a/src/shared/utils/http/revalidate.ts b/src/shared/utils/http/revalidate.ts new file mode 100644 index 0000000..2357250 --- /dev/null +++ b/src/shared/utils/http/revalidate.ts @@ -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)); +}; diff --git a/src/shared/utils/index.ts b/src/shared/utils/index.ts deleted file mode 100644 index c37acc9..0000000 --- a/src/shared/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { getYouTubeID } from "./getYoutubeId"; - -export { getYouTubeID }; diff --git a/src/widgets/header/header.tsx b/src/widgets/header/header.tsx index 0c99128..c3e85fa 100644 --- a/src/widgets/header/header.tsx +++ b/src/widgets/header/header.tsx @@ -7,12 +7,7 @@ import Link from "next/link"; import { useSelectedLayoutSegment } from "next/navigation"; import clsx from "clsx"; import { UserActivities } from "@/features/userActivities"; - -const sections = [ - { title: "Игры", href: "/games" }, - { title: "Фильмы", href: "/movies" }, - { title: "Аудиокниги", href: "/audiobooks" }, -]; +import { ItemSections, ItemService } from "@/entities/item"; export const Header = () => { const currentPageName = useSelectedLayoutSegment(); @@ -25,21 +20,21 @@ export const Header = () => { >

- +
.Torrent

- {sections.map((section) => ( + {ItemSections.map((section) => ( - {section.title} + {ItemService.itemSections[section].sectionName} ))}
diff --git a/src/widgets/header/mobileMenu/mobileMenu.tsx b/src/widgets/header/mobileMenu/mobileMenu.tsx index fee9a33..efa5aee 100644 --- a/src/widgets/header/mobileMenu/mobileMenu.tsx +++ b/src/widgets/header/mobileMenu/mobileMenu.tsx @@ -1,14 +1,11 @@ "use client"; +import { ItemSections, ItemService } from "@/entities/item"; import clsx from "clsx"; import Link from "next/link"; import { useState } from "react"; -export const MobileMenu = ({ - sections, -}: { - sections: { title: string; href: string }[]; -}) => { +export const MobileMenu = () => { const [open, changeMenuOpen] = useState(false); return ( @@ -35,13 +32,13 @@ export const MobileMenu = ({ )} onClick={() => changeMenuOpen(false)} > - {sections.map((section) => ( + {ItemSections.map((section) => ( - {section.title} + {ItemService.itemSections[section].sectionName} ))} diff --git a/src/widgets/itemInfo/itemInfo.tsx b/src/widgets/itemInfo/itemInfo.tsx index d475671..017c529 100644 --- a/src/widgets/itemInfo/itemInfo.tsx +++ b/src/widgets/itemInfo/itemInfo.tsx @@ -1,6 +1,12 @@ "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 useSWR from "swr"; import { useEffect, useRef, useState } from "react"; @@ -12,8 +18,8 @@ import { ItemProperties } from "./itemProperties"; import { ItemTrailer } from "./itemTrailer"; import { ItemTorrent } from "./itemTorrent"; import { ItemDetails } from "./itemDetails"; -import { isMovie } from "@/entities/item/movie/schemas/movie"; import { ItemFragment } from "./itemFragment"; +import { EraseCacheByTag } from "@/shared/utils/http"; export const ItemInfo = ({ item: init_item, diff --git a/src/widgets/itemInfo/itemTorrent.tsx b/src/widgets/itemInfo/itemTorrent.tsx index 7f92a83..1822abd 100644 --- a/src/widgets/itemInfo/itemTorrent.tsx +++ b/src/widgets/itemInfo/itemTorrent.tsx @@ -93,8 +93,8 @@ export const ItemTorrent = ({ className="text-right text-sm relative top-4 lp:-top-4" href="/how_to_download" > - Как скачать игру -
с помощью .torrent файла? + Как скачать с помощью +
.torrent файла? diff --git a/src/widgets/itemInfo/itemTrailer.tsx b/src/widgets/itemInfo/itemTrailer.tsx index b16b428..5051297 100644 --- a/src/widgets/itemInfo/itemTrailer.tsx +++ b/src/widgets/itemInfo/itemTrailer.tsx @@ -1,5 +1,5 @@ import { ItemCreateType, ItemType } from "@/entities/item"; -import { getYouTubeID } from "@/shared/utils"; +import { getYouTubeID } from "@/shared/utils/getYoutubeId"; import { UseFormRegister, UseFormSetValue } from "react-hook-form"; export const ItemTrailer = ({ @@ -9,8 +9,8 @@ export const ItemTrailer = ({ setFormValue: setValue, registerFormField: register, }: { - trailer: string | undefined; - default_trailer: string | undefined; + trailer: string | undefined | null; + default_trailer: string | undefined | null; editable: boolean; setFormValue: UseFormSetValue; registerFormField: UseFormRegister;