Refactoring

This commit is contained in:
2024-06-23 19:43:12 +04:00
parent 7f4d1bf87d
commit ea723b88b0
18 changed files with 224 additions and 223 deletions

View File

@@ -1,5 +1,6 @@
import { GameService, GameType, isSection, ItemService } from "@/entities/item";
import { ItemCard } from "@/features/itemCard";
import { ItemService } from "@/entities/item";
import { SectionService } from "@/features/sections";
import { ItemCard } from "@/widgets/itemCard";
import { ItemInfo } from "@/widgets/itemInfo";
import { Section } from "@/widgets/section";
import { redirect } from "next/navigation";
@@ -9,13 +10,17 @@ export default async function Item({
}: {
params: { section: string; item_id: number };
}) {
const game = isSection(section)
? await ItemService.itemSections[section].service.Get(item_id)
const game = SectionService.isSection(section)
? await ItemService.itemsConfiguration[
SectionService.sectionsConfiguration[section].itemType
].service.Get(item_id)
: redirect("/");
const cards =
isSection(section) &&
(await ItemService.itemSections[section].service.GetCards());
SectionService.isSection(section) &&
(await ItemService.itemsConfiguration[
SectionService.sectionsConfiguration[section].itemType
].service.GetCards());
return (
<>
@@ -24,14 +29,15 @@ export default async function Item({
{cards && (
<Section
name={
isSection(section)
? ItemService.itemSections[section].popularSubsectionName
SectionService.isSection(section)
? SectionService.sectionsConfiguration[section]
.popularSubsectionName
: undefined
}
link={isSection(section) ? `/${section}` : undefined}
link={SectionService.isSection(section) ? `/${section}` : undefined}
invite_text={
isSection(section)
? ItemService.itemSections[section].sectionInviteText
SectionService.isSection(section)
? SectionService.sectionsConfiguration[section].sectionInviteText
: undefined
}
>

View File

@@ -1,21 +1,22 @@
import { GameService, isSection, ItemService } from "@/entities/item";
import { ItemCard } from "@/features/itemCard";
import { ItemCard } from "@/widgets/itemCard";
import { ItemInfo } from "@/widgets/itemInfo";
import { Section } from "@/widgets/section";
import { redirect } from "next/navigation";
import { Metadata } from "next";
import { SectionService } from "@/features/sections";
import { ItemService } from "@/entities/item";
export async function generateMetadata({
params: { section },
}: {
params: { section: string };
}): Promise<Metadata> {
if (!isSection(section)) {
if (!SectionService.isSection(section)) {
redirect("/");
return {};
}
return {
title: `.Torrent: ${ItemService.itemSections[section].addItemText}`,
title: `.Torrent: ${SectionService.sectionsConfiguration[section].addItemText}`,
};
}
@@ -24,13 +25,17 @@ export default async function AddItem({
}: {
params: { section: string };
}) {
const emptyItem = isSection(section)
? await ItemService.itemSections[section].service.GetEmpty()
const emptyItem = SectionService.isSection(section)
? await ItemService.itemsConfiguration[
SectionService.sectionsConfiguration[section].itemType
].service.GetEmpty()
: redirect("/");
const cards =
isSection(section) &&
(await ItemService.itemSections[section].service.GetCards());
SectionService.isSection(section) &&
(await ItemService.itemsConfiguration[
SectionService.sectionsConfiguration[section].itemType
].service.GetCards());
return (
<>
@@ -39,14 +44,15 @@ export default async function AddItem({
{cards && (
<Section
name={
isSection(section)
? ItemService.itemSections[section].popularSubsectionName
SectionService.isSection(section)
? SectionService.sectionsConfiguration[section]
.popularSubsectionName
: undefined
}
link={isSection(section) ? `/${section}` : undefined}
link={SectionService.isSection(section) ? `/${section}` : undefined}
invite_text={
isSection(section)
? ItemService.itemSections[section].sectionInviteText
SectionService.isSection(section)
? SectionService.sectionsConfiguration[section].sectionInviteText
: undefined
}
>

View File

@@ -1,21 +1,25 @@
import { isSection, ItemService, MovieService } from "@/entities/item";
import { ItemCard } from "@/features/itemCard";
import { ItemCard } from "@/widgets/itemCard";
import { Section } from "@/widgets/section";
import { redirect } from "next/navigation";
import { Metadata } from "next";
import { SectionService } from "@/features/sections";
export async function generateMetadata({
params: { section },
}: {
params: { section: string };
}): Promise<Metadata> {
if (!isSection(section)) {
if (!SectionService.isSection(section)) {
redirect("/");
return {};
}
return {
title: `.Torrent: ${ItemService.itemSections[section].sectionName}`,
description: `.Torrent: ${ItemService.itemSections[section].sectionName} - ${ItemService.itemSections[section].sectionName}`,
title: `.Torrent: ${SectionService.sectionsConfiguration[section].sectionName}`,
description:
`.Torrent: ` +
`${SectionService.sectionsConfiguration[section].sectionName} - ` +
`${SectionService.sectionsConfiguration[section].sectionDescription}`,
};
}
@@ -24,8 +28,10 @@ export default async function SectionPage({
}: {
params: { section: string };
}) {
const cards = isSection(section)
? await ItemService.itemSections[section].service.GetCards()
const cards = SectionService.isSection(section)
? await ItemService.itemsConfiguration[
SectionService.sectionsConfiguration[section].itemType
].service.GetCards()
: redirect("/");
return (

View File

@@ -1,11 +1,6 @@
import {
isSection,
ItemCardType,
ItemSections,
ItemSectionsType,
ItemService,
} from "@/entities/item";
import { ItemCard } from "@/features/itemCard";
import { ItemCardType, ItemService } from "@/entities/item";
import { ItemCard } from "@/widgets/itemCard";
import { SectionService, SectionType } from "@/features/sections";
import { Section } from "@/widgets/section";
import { Metadata } from "next";
@@ -16,24 +11,31 @@ export const metadata: Metadata = {
};
export default async function Home() {
const cards: { [k in ItemSectionsType]?: ItemCardType[] | null } = {};
const cards: { [k in SectionType]?: ItemCardType[] | null } = {};
await Promise.all(
ItemSections.map(async (section) => {
cards[section] = await ItemService.itemSections[
section
SectionService.sections.map(async (section) => {
cards[section] = await ItemService.itemsConfiguration[
SectionService.sectionsConfiguration[section].itemType
].service.GetCards();
})
);
return (
<>
{ItemSections.map((section) => (
{SectionService.sections.map((section) => (
<section key={section}>
{cards[section] && cards[section].length > 0 && (
<Section
name={ItemService.itemSections[section].popularSubsectionName}
link={isSection(section) ? `/${section}` : undefined}
invite_text={ItemService.itemSections[section].sectionInviteText}
name={
SectionService.sectionsConfiguration[section]
.popularSubsectionName
}
link={
SectionService.isSection(section) ? `/${section}` : undefined
}
invite_text={
SectionService.sectionsConfiguration[section].sectionInviteText
}
>
{cards[section].map((card) => (
<ItemCard key={card.id} card={card} />

View File

@@ -85,20 +85,16 @@ import { ItemService } from "./item";
export { ItemService };
import {
isSection,
TypesOfItems,
type IItemService,
type ItemType,
type ItemCardType,
type ItemCreateType,
type TypesOfItems,
type ItemSectionsType,
ItemSections,
} from "./types";
export {
isSection,
TypesOfItems,
type IItemService,
type ItemType,
type ItemCardType,
type ItemCreateType,
type TypesOfItems,
type ItemSectionsType,
ItemSections,
};

View File

@@ -7,10 +7,8 @@ import { audiobookCreateSchema } from "./audiobook/schemas/audiobook";
import { AudiobookService } from "./audiobook/audiobook";
import {
IItemService,
ItemCardType,
ItemCreateType,
ItemPropertiesDescriptionType,
ItemSectionsType,
ItemType,
TypesOfItems,
UnionItemType,
@@ -18,9 +16,8 @@ import {
import { EraseCacheByTags } from "@/shared/utils/http";
export abstract class ItemService {
private static get itemsConfiguration(): {
static get itemsConfiguration(): {
[k in TypesOfItems]: {
sectionUrl: ItemSectionsType;
formResolver: ZodSchema;
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
service: IItemService;
@@ -28,19 +25,16 @@ export abstract class ItemService {
} {
return {
[TypesOfItems.game]: {
sectionUrl: "games",
formResolver: gameCreateSchema,
propertiesDescription: GameService.propertiesDescription,
service: GameService,
},
[TypesOfItems.movie]: {
sectionUrl: "movies",
formResolver: movieCreateSchema,
propertiesDescription: MovieService.propertiesDescription,
service: MovieService,
},
[TypesOfItems.audiobook]: {
sectionUrl: "audiobooks",
formResolver: audiobookCreateSchema,
propertiesDescription: AudiobookService.propertiesDescription,
service: AudiobookService,
@@ -48,76 +42,12 @@ export abstract class ItemService {
};
}
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,
},
};
}
public static isExistingItem(
item: ItemCreateType | ItemType
): item is ItemType {
return (item as ItemType).id !== undefined;
}
public static GetFormResolver(
item: ItemCardType | ItemCreateType | ItemType
) {
return this.itemsConfiguration[item.type].formResolver;
}
public static GetSectionUrlByItemType(
item: ItemCardType | ItemCreateType | ItemType
) {
return this.itemsConfiguration[item.type].sectionUrl;
}
public static GetPropertiesDescriptionForItem<
T extends ItemType | ItemCreateType
>(item: T) {
return this.itemsConfiguration[item.type]
.propertiesDescription as ItemPropertiesDescriptionType<T>;
}
public static async AddItem(itemInfo: ItemCreateType) {
const item = await this.itemsConfiguration[itemInfo.type].service.Add(
itemInfo

View File

@@ -16,30 +16,12 @@ export type ItemCreateType =
| AudiobookCreateType;
export type UnionItemType = GameType & MovieType & AudiobookType;
export type UnionItemCardType = GameCardType &
MovieCardType &
AudiobookCardType;
export type UnionItemCreateType = GameCreateType &
MovieCreateType &
AudiobookCreateType;
export enum TypesOfItems {
game,
movie,
audiobook,
}
export type ItemSectionsType = "games" | "movies" | "audiobooks";
export const ItemSections = [
"games",
"movies",
"audiobooks",
] as ItemSectionsType[];
export const isSection = (a: string): a is ItemSectionsType => {
return (ItemSections as string[]).includes(a);
};
export type ItemPropertiesDescriptionType<T extends ItemType | ItemCreateType> =
{
name: string;

View File

@@ -0,0 +1,2 @@
import { SectionService, type SectionType } from "./sections";
export { SectionService, type SectionType };

View File

@@ -0,0 +1,64 @@
import { TypesOfItems } from "@/entities/item";
export type SectionType = (typeof SectionService.sections)[number];
export abstract class SectionService {
static get itemTypeToSection(): { [k in TypesOfItems]: SectionType } {
return {
[TypesOfItems.game]: "games",
[TypesOfItems.movie]: "movies",
[TypesOfItems.audiobook]: "audiobooks",
};
}
static get sectionsConfiguration(): {
[k in SectionType]: {
sectionName: string;
sectionUrl: string;
itemType: TypesOfItems;
popularSubsectionName: string;
sectionInviteText: string;
addItemText: string;
sectionDescription: string;
};
} {
return {
games: {
sectionName: "Игры",
sectionUrl: "games",
itemType: TypesOfItems.game,
popularSubsectionName: "Популярные игры",
sectionInviteText: 'Перейти в раздел "Игры"',
addItemText: "Добавить игру",
sectionDescription:
"каталог .torrent файлов для обмена актуальными версиями популярных игр",
},
movies: {
sectionName: "Фильмы",
sectionUrl: "movies",
itemType: TypesOfItems.movie,
popularSubsectionName: "Популярные фильмы",
sectionInviteText: 'Перейти в раздел "Фильмы"',
addItemText: "Добавить фильм",
sectionDescription:
"каталог .torrent файлов для обмена популярными фильмами в лучшем качестве",
},
audiobooks: {
sectionName: "Аудиокниги",
sectionUrl: "audiobooks",
itemType: TypesOfItems.audiobook,
popularSubsectionName: "Популярные аудиокниги",
sectionInviteText: 'Перейти в раздел "Аудиокниги"',
addItemText: "Добавить аудиокнигу",
sectionDescription:
"каталог .torrent файлов для обмена популярными аудиокнигами любимых авторов",
},
};
}
static sections = ["games", "movies", "audiobooks"] as const;
static isSection = (a: string): a is SectionType => {
return this.sections.includes(a as SectionType);
};
}

View File

@@ -7,7 +7,7 @@ import useSWR, { mutate } from "swr";
import clsx from "clsx";
import Cookies from "js-cookie";
import { useState } from "react";
import { ItemService } from "@/entities/item";
import { SectionService } from "../sections";
export const UserActivities = () => {
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
@@ -39,14 +39,13 @@ export const UserActivities = () => {
{[
{
group: "Добавить:",
items: Object.entries(ItemService.itemSections).map(
([sectionId, section]) => {
return {
name: section.addItemText,
link: `/${sectionId}/add`,
};
}
),
items: SectionService.sections.map((section) => {
return {
name: SectionService.sectionsConfiguration[section]
.addItemText,
link: `/${SectionService.sectionsConfiguration[section].sectionUrl}/add`,
};
}),
},
{
name: "Выйти",

View File

@@ -33,7 +33,6 @@ export abstract class HTTPService {
schema: Z,
options?: RequestOptions
) {
console.log(options?.body);
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
method: method,
headers: {

View File

@@ -7,7 +7,7 @@ import Link from "next/link";
import { useSelectedLayoutSegment } from "next/navigation";
import clsx from "clsx";
import { UserActivities } from "@/features/userActivities";
import { ItemSections, ItemService } from "@/entities/item";
import { SectionService } from "@/features/sections";
export const Header = () => {
const currentPageName = useSelectedLayoutSegment();
@@ -25,7 +25,7 @@ export const Header = () => {
<Link href="/">.Torrent</Link>
</h1>
<div className="hidden text-2xl lp:block">
{ItemSections.map((section) => (
{SectionService.sections.map((section) => (
<Link
key={section}
className={clsx(
@@ -34,7 +34,7 @@ export const Header = () => {
)}
href={"/" + section}
>
{ItemService.itemSections[section].sectionName}
{SectionService.sectionsConfiguration[section].sectionName}
</Link>
))}
</div>

View File

@@ -1,6 +1,6 @@
"use client";
import { ItemSections, ItemService } from "@/entities/item";
import { SectionService } from "@/features/sections";
import clsx from "clsx";
import Link from "next/link";
import { useState } from "react";
@@ -32,13 +32,13 @@ export const MobileMenu = () => {
)}
onClick={() => changeMenuOpen(false)}
>
{ItemSections.map((section) => (
{SectionService.sections.map((section) => (
<Link
key={section}
className="text-xl py-2 cursor-pointer hover:underline"
href={"/" + section}
>
{ItemService.itemSections[section].sectionName}
{SectionService.sectionsConfiguration[section].sectionName}
</Link>
))}
</div>

View File

@@ -5,6 +5,7 @@ import {
ItemCardType,
ItemService,
} from "@/entities/item";
import { SectionService } from "@/features/sections";
import { Img } from "@/shared/ui";
import Link from "next/link";
@@ -12,7 +13,14 @@ export const ItemCard = ({ card }: { card: ItemCardType }) => {
return (
<Link
className="group/itemcard cursor-pointer"
href={"/" + ItemService.GetSectionUrlByItemType(card) + "/" + card.id}
href={
"/" +
SectionService.sectionsConfiguration[
SectionService.itemTypeToSection[card.type]
].sectionUrl +
"/" +
card.id
}
>
{!!card.cover && (
<Img

View File

@@ -44,15 +44,15 @@ export const ItemInfo = <T extends ItemType | ItemCreateType>({
register,
handleSubmit,
setValue,
setError,
watch,
reset,
formState: { dirtyFields, errors },
} = useForm<ItemType | ItemCreateType>({
// Unfortunately, react hook form does not accept generic type correctly
// useForm<T> causes an error when calling register(key) ->
// key is not assignable to parameter of type 'Path<T>'
defaultValues: init_item,
resolver: zodResolver(ItemService.GetFormResolver(item)),
resolver: zodResolver(
ItemService.itemsConfiguration[item.type].formResolver
),
});
useEffect(() => {
@@ -72,7 +72,6 @@ export const ItemInfo = <T extends ItemType | ItemCreateType>({
useEffect(() => {
if (!Object.keys(dirtyFields).length) return;
if (JSON.stringify(watchedData) === JSON.stringify(formData)) return;
console.log(dirtyFields);
changeFormData(watchedData as T);
if (savedTimeout) clearTimeout(savedTimeout);
changeSavedTimeout(
@@ -83,14 +82,15 @@ export const ItemInfo = <T extends ItemType | ItemCreateType>({
}, [watchedData]);
const onSubmit = async (formData: ItemCreateType) => {
changeSavedTimeout(null);
console.log(formData);
const updatedItem = ItemService.isExistingItem(item)
? await ItemService.ChangeItem(item.id, formData)
: await ItemService.AddItem(formData);
changeSavedTimeout(null);
if (updatedItem) {
changeItem(updatedItem as T);
reset({}, { keepValues: true });
} else {
setError("root", { message: "Ошибка сервера" });
}
};

View File

@@ -1,5 +1,6 @@
import { ItemCreateType, ItemType } from "@/entities/item";
import { ItemService } from "@/entities/item/item";
import { ItemPropertiesDescriptionType } from "@/entities/item/types";
import clsx from "clsx";
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
@@ -23,62 +24,63 @@ export const ItemProperties = <T extends ItemType | ItemCreateType>({
!editable && "cursor-default"
)}
>
{(ItemService.GetPropertiesDescriptionForItem(item) ?? []).map(
(section, i) => (
<ul key={i} className="w-[48%] bg-bg1 rounded-lg py-1 px-4">
{section.map((req) => (
<li key={req.name} className="text-sm lp:text-md py-1">
<span className="font-bold">{req.name + ": "}</span>
<span className="relative">
<span
className={clsx(
"text-fg4 opacity-0 absolute -z-10 text-xs",
(watchedData[req.key] === undefined ||
watchedData[req.key] === null ||
(watchedData[req.key] as string) === "") &&
"opacity-100 relative !z-10 text-base",
"transition-opacity "
)}
>
Не известно
</span>
<span
className={clsx(
"outline-none",
!editable && "cursor-default",
(watchedData[req.key] === undefined ||
watchedData[req.key] === null ||
(watchedData[req.key] as string) === "") &&
"opacity-100 absolute left-0 top-0 inline-block min-w-10 z-10"
)}
{...register(req.key as keyof ItemType, {
value: req.value
? req.value(item)
: undefined ?? (item[req.key] as string),
})}
contentEditable={editable && (req.editable ?? true)}
suppressContentEditableWarning={true}
onInput={(e) => {
setValue(
req.key as keyof ItemType,
e.currentTarget.innerText,
{
shouldValidate: true,
shouldDirty: true,
}
);
}}
>
{req.value
? req.value(item)
: undefined ?? (item[req.key] as string)}
</span>
{(
ItemService.itemsConfiguration[item.type]
.propertiesDescription as ItemPropertiesDescriptionType<T>
).map((section, i) => (
<ul key={i} className="w-[48%] bg-bg1 rounded-lg py-1 px-4">
{section.map((req) => (
<li key={req.name} className="text-sm lp:text-md py-1">
<span className="font-bold">{req.name + ": "}</span>
<span className="relative">
<span
className={clsx(
"text-fg4 opacity-0 absolute -z-10 text-xs",
(watchedData[req.key] === undefined ||
watchedData[req.key] === null ||
(watchedData[req.key] as string) === "") &&
"opacity-100 relative !z-10 text-base",
"transition-opacity "
)}
>
Не известно
</span>
</li>
))}
</ul>
)
)}
<span
className={clsx(
"outline-none",
!editable && "cursor-default",
(watchedData[req.key] === undefined ||
watchedData[req.key] === null ||
(watchedData[req.key] as string) === "") &&
"opacity-100 absolute left-0 top-0 inline-block min-w-10 z-10"
)}
{...register(req.key as keyof ItemType, {
value: req.value
? req.value(item)
: undefined ?? (item[req.key] as string),
})}
contentEditable={editable && (req.editable ?? true)}
suppressContentEditableWarning={true}
onInput={(e) => {
setValue(
req.key as keyof ItemType,
e.currentTarget.innerText,
{
shouldValidate: true,
shouldDirty: true,
}
);
}}
>
{req.value
? req.value(item)
: undefined ?? (item[req.key] as string)}
</span>
</span>
</li>
))}
</ul>
))}
</div>
);
};

View File

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