diff --git a/src/app/@auth/(.)login/page.tsx b/src/app/@auth/(.)login/page.tsx index c332980..2e2cb2f 100644 --- a/src/app/@auth/(.)login/page.tsx +++ b/src/app/@auth/(.)login/page.tsx @@ -1,9 +1,9 @@ "use client"; import { - LoginForm, - loginFormFieldNames, - loginFormSchema, + LoginForm, + loginFormFieldNames, + loginFormSchema, } from "@/entities/user"; import { UserService } from "@/entities/user/user"; import { Modal } from "@/shared/ui"; @@ -13,60 +13,60 @@ import { SubmitHandler, useForm } from "react-hook-form"; import { mutate } from "swr"; export default function Login() { - const { - register, - handleSubmit, - formState: { errors }, - } = useForm({ resolver: zodResolver(loginFormSchema) }); + const { + register, + handleSubmit, + formState: { errors }, + } = useForm({ resolver: zodResolver(loginFormSchema) }); - const router = useRouter(); + const router = useRouter(); - const onSubmit: SubmitHandler = async (data) => { - const userInfo = await UserService.Login(data); - mutate("user", userInfo); - router.back(); - }; + const onSubmit: SubmitHandler = async (data) => { + const userInfo = await UserService.Login(data); + mutate("user", userInfo); + router.back(); + }; - return ( - -
-
-

.Torrent

- {(["username", "password"] as (keyof LoginForm)[]).map((field) => ( - + ))} - -
-
-
- ); + + + + + ); } diff --git a/src/app/games/[game_id]/page.tsx b/src/app/games/[game_id]/page.tsx index 38aeb98..891cb76 100644 --- a/src/app/games/[game_id]/page.tsx +++ b/src/app/games/[game_id]/page.tsx @@ -4,27 +4,27 @@ import { GameInfo } from "@/widgets/gameInfo"; import { Section } from "@/widgets/section"; export default async function Games({ - params: { game_id }, + params: { game_id }, }: { - params: { game_id: number }; + params: { game_id: number }; }) { - const gameCards = await GameService.getGameCards(); - const game = await GameService.getGame(game_id); - return ( - <> - {game && } + const gameCards = await GameService.GetGameCards(); + const game = await GameService.GetGame(game_id); + return ( + <> + {game && } - {gameCards && ( -
- {gameCards.map((card) => ( - - ))} -
- )} - - ); + {gameCards && ( +
+ {gameCards.map((card) => ( + + ))} +
+ )} + + ); } diff --git a/src/app/games/add/page.tsx b/src/app/games/add/page.tsx new file mode 100644 index 0000000..658e902 --- /dev/null +++ b/src/app/games/add/page.tsx @@ -0,0 +1,26 @@ +import { GameService } from "@/entities/game"; +import { GameCard } from "@/features/gameCard"; +import { GameInfo } from "@/widgets/gameInfo"; +import { Section } from "@/widgets/section"; + +export default async function AddGame() { + const gameCards = await GameService.GetGameCards(); + + return ( + <> + + + {gameCards && ( +
+ {gameCards.map((card) => ( + + ))} +
+ )} + + ); +} diff --git a/src/app/games/page.tsx b/src/app/games/page.tsx index 869fa6a..fd11cb8 100644 --- a/src/app/games/page.tsx +++ b/src/app/games/page.tsx @@ -10,7 +10,7 @@ export const metadata: Metadata = { }; export default async function Games() { - const gameCards = await GameService.getGameCards(); + const gameCards = await GameService.GetGameCards(); return ( <> {gameCards && gameCards.length > 0 && ( diff --git a/src/app/how_to_download/page.tsx b/src/app/how_to_download/page.tsx index 6b04415..4b2ac52 100644 --- a/src/app/how_to_download/page.tsx +++ b/src/app/how_to_download/page.tsx @@ -1,61 +1,61 @@ import { Metadata } from "next"; export const metadata: Metadata = { - title: ".Torrent: Как скачать?", - description: - ".Torrent: Как скачать? - краткое руководство по скачиваю данных с помощью .torrent файлов", + title: ".Torrent: Как скачать?", + description: + ".Torrent: Как скачать? - краткое руководство по скачиваю данных с помощью .torrent файлов", }; export default async function HowToDownload() { - return ( -
-
-

Как скачать?

-
- Чтобы скачать данные с помощью торрент-файла, выполните следующие - шаги: -
    -
  • - 1. Загрузите торрент-файл, содержащий информацию о файлах, которые - вы хотите скачать с нашего сайта. -
  • -
  • - 2. Откройте программу-клиент для загрузки торрентов, например, - uTorrent, BitTorrent или qBittorrent. -
  • -
  • - 3. В программе-клиенте выберите опцию "Open Torrent File" или "Add - Torrent" и выберите торрент-файл, который вы скачали в первом - шаге. -
  • -
  • - 4. После этого начнется загрузка файлов, указанных в - торрент-файле. Вы также можете выбрать папку, куда сохранять - скачанные файлы. -
  • -
  • - 5. Дождитесь завершения загрузки файлов. После этого вы сможете - открыть и использовать скачанные файлы на своем компьютере. -
  • -
-
-
-
-

Что такое .torrent файл?

-

- Торрент-файл (или .torrent-файл) - это небольшой файл, который - содержит метаданные о файле или наборе файлов, которые можно загрузить - с помощью протокола BitTorrent. В торрент-файле обычно указан адрес - трекера (специального сервера, отслеживающего пиров) и хеш-суммы - частей файлов, которые необходимы для скачивания. -
-
Пользователь, желающий загрузить файл через BitTorrent, сначала - скачивает торрент-файл или magnet-ссылку, загружает ее в - торрент-клиент (программу для скачивания торрентов), и затем начинает - загрузку файлов, участвуя в обмене данными с другими пользователями - (пирами) через сеть BitTorrent. -

-
-
- ); + return ( +
+
+

Как скачать?

+
+ Чтобы скачать данные с помощью торрент-файла, выполните следующие + шаги: +
    +
  • + 1. Загрузите торрент-файл, содержащий информацию о файлах, которые + вы хотите скачать с нашего сайта. +
  • +
  • + 2. Откройте программу-клиент для загрузки торрентов, например, + uTorrent, BitTorrent или qBittorrent. +
  • +
  • + 3. В программе-клиенте выберите опцию "Open Torrent + File" или "Add Torrent" и выберите торрент-файл, + который вы скачали в первом шаге. +
  • +
  • + 4. После этого начнется загрузка файлов, указанных в + торрент-файле. Вы также можете выбрать папку, куда сохранять + скачанные файлы. +
  • +
  • + 5. Дождитесь завершения загрузки файлов. После этого вы сможете + открыть и использовать скачанные файлы на своем компьютере. +
  • +
+
+
+
+

Что такое .torrent файл?

+

+ Торрент-файл (или .torrent-файл) - это небольшой файл, который + содержит метаданные о файле или наборе файлов, которые можно загрузить + с помощью протокола BitTorrent. В торрент-файле обычно указан адрес + трекера (специального сервера, отслеживающего пиров) и хеш-суммы + частей файлов, которые необходимы для скачивания. +
+
Пользователь, желающий загрузить файл через BitTorrent, сначала + скачивает торрент-файл или magnet-ссылку, загружает ее в + торрент-клиент (программу для скачивания торрентов), и затем начинает + загрузку файлов, участвуя в обмене данными с другими пользователями + (пирами) через сеть BitTorrent. +

+
+
+ ); } diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a7ad26c..b2e459b 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -6,27 +6,27 @@ import { Header } from "@/widgets/header"; const inter = Inter({ subsets: ["latin"] }); export default function RootLayout({ - auth, - children, + auth, + children, }: Readonly<{ - auth: React.ReactNode; - children: React.ReactNode; + auth: React.ReactNode; + children: React.ReactNode; }>) { - return ( - // suppressHydrationWarning for theme support - - - - {auth} -
-
+ + + {auth} +
+
- {children} -
- - - - ); + > + {children} +
+ + + + ); } diff --git a/src/app/login/page.tsx b/src/app/login/page.tsx index 7d37d98..e1c910d 100644 --- a/src/app/login/page.tsx +++ b/src/app/login/page.tsx @@ -1,5 +1,5 @@ import { redirect } from "next/navigation"; export default function Login() { - redirect("/"); + redirect("/"); } diff --git a/src/app/page.tsx b/src/app/page.tsx index 482bb9e..88ad412 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -4,26 +4,26 @@ import { Section } from "@/widgets/section"; import { Metadata } from "next"; export const metadata: Metadata = { - title: ".Torrent", - description: - ".Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг", + title: ".Torrent", + description: + ".Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг", }; export default async function Home() { - const gameCards = await GameService.getGameCards(); - return ( - <> - {gameCards && gameCards.length > 0 && ( -
- {gameCards.map((card) => ( - - ))} -
- )} - - ); + const gameCards = await GameService.GetGameCards(); + return ( + <> + {gameCards && gameCards.length > 0 && ( +
+ {gameCards.map((card) => ( + + ))} +
+ )} + + ); } diff --git a/src/entities/files/files.ts b/src/entities/files/files.ts index c08dc4c..51583a2 100644 --- a/src/entities/files/files.ts +++ b/src/entities/files/files.ts @@ -13,4 +13,16 @@ export abstract class FilesService { false ); } + + public static async UploadTorrent(torrent: File) { + const formData = new FormData(); + formData.append("torrent", torrent); + return await HTTPService.post( + `/files/torrent`, + coverNameSchema, + formData, + {}, + false + ); + } } diff --git a/src/entities/game/game.ts b/src/entities/game/game.ts index 86647b9..4a17508 100644 --- a/src/entities/game/game.ts +++ b/src/entities/game/game.ts @@ -1,16 +1,25 @@ import { HTTPService } from "@/shared/utils/http"; -import { gameCardsSchema, GameCardType } from "./schemas/gameCard"; +import { gameCardsSchema } from "./schemas/gameCard"; import { GameCreateType, gameSchema } from "./schemas/game"; -import { z } from "zod"; export abstract class GameService { - public static async getGameCards() { - return await HTTPService.get("/games/cards", gameCardsSchema); - } - public static async getGame(id: number) { - return await HTTPService.get(`/games/${id}`, gameSchema); - } - public static async changeGame(id: number, gameInfo: GameCreateType) { - return await HTTPService.put(`/games/${id}`, gameSchema, gameInfo); - } + public static async GetGameCards() { + return await HTTPService.get("/games/cards", gameCardsSchema); + } + public static async GetGame(id: number) { + return await HTTPService.get(`/games/${id}`, gameSchema); + } + public static async ChangeGame(id: number, gameInfo: GameCreateType) { + return await HTTPService.put(`/games/${id}`, gameSchema, gameInfo); + } + public static async AddGame(gameInfo: GameCreateType) { + return await HTTPService.post(`/games`, gameSchema, gameInfo); + } + + public static GetEmptyGame(): GameCreateType { + return { + title: "", + torrent_file: "", + }; + } } diff --git a/src/entities/game/index.ts b/src/entities/game/index.ts index 3ff1f88..db43b3b 100644 --- a/src/entities/game/index.ts +++ b/src/entities/game/index.ts @@ -3,9 +3,9 @@ import { GameType, GameCreateType, gameCreateSchema } from "./schemas/game"; import { GameService } from "./game"; export { - GameService, - gameCreateSchema, - type GameType, - type GameCreateType, - type GameCardType, + GameService, + gameCreateSchema, + type GameType, + type GameCreateType, + type GameCardType, }; diff --git a/src/entities/game/schemas/game.ts b/src/entities/game/schemas/game.ts index 5d6ac12..c0fb88a 100644 --- a/src/entities/game/schemas/game.ts +++ b/src/entities/game/schemas/game.ts @@ -3,23 +3,30 @@ import { gameCardBaseSchema } from "./gameCard"; export const gameBaseSchema = gameCardBaseSchema.merge( z.object({ - torrent_file: z.string().min(1), + torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"), trailer: z.string().optional(), - system: z.string().optional(), - processor: z.string().optional(), - memory: z.string().optional(), - graphics: z.string().optional(), - storage: z.string().optional(), + system: z.string().optional().nullable(), + processor: z.string().optional().nullable(), + memory: z.string().optional().nullable(), + graphics: z.string().optional().nullable(), + storage: z.string().optional().nullable(), - developer: z.string().optional(), - language: z.string().optional(), - download_size: z.string().optional(), + developer: z.string().optional().nullable(), + language: z.string().optional().nullable(), + download_size: z.string().optional().nullable(), release_date: z .string() - .min(1) - .transform((d) => new Date(d)), + .optional() + .nullable() + .transform((d) => + d + ? new Date(d).toLocaleDateString("en-us", { + year: "numeric", + }) + : undefined + ), }) ); diff --git a/src/entities/game/schemas/gameCard.ts b/src/entities/game/schemas/gameCard.ts index 0d7ebad..4bf3b8b 100644 --- a/src/entities/game/schemas/gameCard.ts +++ b/src/entities/game/schemas/gameCard.ts @@ -1,10 +1,10 @@ import { z } from "zod"; export const gameCardBaseSchema = z.object({ - title: z.string().min(3), - cover: z.string().optional(), - description: z.string().optional(), - version: z.string().optional(), + title: z.string().min(3, "Слишком короткое название"), + cover: z.string().optional().nullable(), + description: z.string().optional().nullable(), + version: z.string().optional().nullable(), }); export const gameCardSchema = gameCardBaseSchema.merge( diff --git a/src/entities/user/index.ts b/src/entities/user/index.ts index f4dfbc5..95c97e5 100644 --- a/src/entities/user/index.ts +++ b/src/entities/user/index.ts @@ -1,16 +1,16 @@ import { - loginFormSchema, - loginFormFieldNames, - LoginForm, + loginFormSchema, + loginFormFieldNames, + LoginForm, } from "./schemas/auth"; import { userSchema, User } from "./schemas/user"; import { UserService } from "./user"; export { - loginFormSchema, - loginFormFieldNames, - UserService, - userSchema, - type User, - type LoginForm, + loginFormSchema, + loginFormFieldNames, + UserService, + userSchema, + type User, + type LoginForm, }; diff --git a/src/entities/user/schemas/auth.ts b/src/entities/user/schemas/auth.ts index 1a14f96..1ef06e0 100644 --- a/src/entities/user/schemas/auth.ts +++ b/src/entities/user/schemas/auth.ts @@ -2,29 +2,29 @@ import { z } from "zod"; import { userSchema } from "./user"; export const loginFormSchema = z.object({ - username: z.string().min(3, "Логин слишком короткий"), - password: z.string().min(3, "Пароль слишком короткий"), + username: z.string().min(3, "Логин слишком короткий"), + password: z.string().min(3, "Пароль слишком короткий"), }); export const loginFormFieldNames = { - username: "Логин", - password: "Пароль", + username: "Логин", + password: "Пароль", }; export type LoginForm = z.infer; export const tokenResponseSchema = z - .object({ - access_token: z.string(), - token_type: z.string(), - }) - .transform((tokenResponse) => tokenResponse.access_token); + .object({ + access_token: z.string(), + token_type: z.string(), + }) + .transform((tokenResponse) => tokenResponse.access_token); export type TokenResponse = z.infer; export const tokenDataSchema = userSchema.merge( - z.object({ - expire: z - .string() - .min(1) - .transform((d) => new Date(d)), - }) + z.object({ + expire: z + .string() + .min(1) + .transform((d) => new Date(d)), + }) ); export type TokenData = z.infer; diff --git a/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx b/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx index fd7291f..8255fa8 100644 --- a/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx +++ b/src/features/colorSchemeSwitch/colorSchemeSwitch.tsx @@ -4,14 +4,14 @@ import { SunIcon } from "@/shared/assets/icons"; import { useTheme } from "next-themes"; export const ColorSchemeSwitch = () => { - const { theme, setTheme } = useTheme(); + const { theme, setTheme } = useTheme(); - return ( - <> - setTheme(theme == "light" ? "dark" : "light")} - /> - - ); + return ( + <> + setTheme(theme == "light" ? "dark" : "light")} + /> + + ); }; diff --git a/src/shared/ui/modal.tsx b/src/shared/ui/modal.tsx index 3630887..b8871a6 100644 --- a/src/shared/ui/modal.tsx +++ b/src/shared/ui/modal.tsx @@ -5,29 +5,29 @@ import { useState } from "react"; import { useRouter } from "next/navigation"; export const Modal = ({ children }: { children: React.ReactNode }) => { - const [closing, setClosing] = useState(false); - const router = useRouter(); - return ( -
{ - setClosing(true); - setTimeout(() => router.back(), 500); - }} - > -
{ - e.stopPropagation(); - }} - > - {children} -
-
- ); + const [closing, setClosing] = useState(false); + const router = useRouter(); + return ( +
{ + setClosing(true); + setTimeout(() => router.back(), 500); + }} + > +
{ + e.stopPropagation(); + }} + > + {children} +
+
+ ); }; diff --git a/src/shared/utils/getYoutubeId.ts b/src/shared/utils/getYoutubeId.ts index 6f77763..cce711e 100644 --- a/src/shared/utils/getYoutubeId.ts +++ b/src/shared/utils/getYoutubeId.ts @@ -1,6 +1,6 @@ export const getYouTubeID = (url: string) => { - const regExp = - /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; - const match = url.match(regExp); - return match && match[7].length == 11 ? match[7] : false; + const regExp = + /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; + const match = url.match(regExp); + return match && match[7].length == 11 ? match[7] : false; }; diff --git a/src/shared/utils/http.ts b/src/shared/utils/http.ts index 67a4609..1e037c1 100644 --- a/src/shared/utils/http.ts +++ b/src/shared/utils/http.ts @@ -31,7 +31,11 @@ export abstract class HTTPService { else throw Error("Response ok = false"); }) .then((r) => r.json()) - .then((d) => schema.parse(d) as z.infer) + .then((d) => { + const parsed = schema.safeParse(d); + if (parsed.success) return parsed.data as z.infer; + else throw new Error(parsed.error.message); + }) .catch((e) => { console.error(e); return null; diff --git a/src/widgets/gameInfo/gameInfo.tsx b/src/widgets/gameInfo/gameInfo.tsx index b179817..eba9752 100644 --- a/src/widgets/gameInfo/gameInfo.tsx +++ b/src/widgets/gameInfo/gameInfo.tsx @@ -11,7 +11,7 @@ import Link from "next/link"; import { getYouTubeID } from "@/shared/utils"; import { UserService } from "@/entities/user"; import useSWR from "swr"; -import { useCallback, useEffect, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState, useMemo } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { Img } from "@/shared/ui"; @@ -19,28 +19,44 @@ import { useDropzone } from "react-dropzone"; import { FilesService } from "@/entities/files"; import { SpinnerIcon } from "@/shared/assets/icons"; -const propertyUnknownText = "Не известно"; +const isExistingGame = (game: GameCreateType | GameType): game is GameType => { + return (game as GameType).id !== undefined; +}; + +export const GameInfo = ({ + game: init_game, +}: { + game: GameCreateType | GameType; +}) => { + const [game, changeGame] = useState(init_game); -export const GameInfo = ({ game }: { game: GameType }) => { const { data: me } = useSWR("user", () => UserService.IdentifyYourself()); const [editable, setEditable] = useState(false); - useEffect(() => setEditable(me?.id === game.owner_id), [me, game]); + useEffect(() => { + if (me) { + if (isExistingGame(game)) setEditable(me.id === game.owner_id); + else setEditable(true); + } + }, [me, game]); + const formRef = useRef(null); const { register, handleSubmit, setValue, watch, + reset, formState: { dirtyFields, errors }, } = useForm({ + defaultValues: game, resolver: zodResolver(gameCreateSchema), }); useEffect(() => { register("torrent_file", { value: game.torrent_file }); register("cover", { value: game.cover }); - }, []); + }, [game.cover, game.torrent_file, register]); const [savedTimeout, changeSavedTimeout] = useState( null @@ -48,6 +64,7 @@ export const GameInfo = ({ game }: { game: GameType }) => { const watchedData = watch(); const [formData, changeFormData] = useState(null); useEffect(() => { + console.log(watchedData); if (!Object.keys(dirtyFields).length) return; if (JSON.stringify(watchedData) === JSON.stringify(formData)) return; console.log(dirtyFields); @@ -55,76 +72,141 @@ export const GameInfo = ({ game }: { game: GameType }) => { if (savedTimeout) clearTimeout(savedTimeout); changeSavedTimeout( setTimeout(() => { + console.log("call", formRef.current); if (formRef.current) formRef.current.requestSubmit(); - }, 5000) + }, 3000) ); }, [watchedData]); const onSubmit = async (formData: GameCreateType) => { changeSavedTimeout(null); - const updatedGame = await GameService.changeGame(game.id, formData); - console.log(updatedGame); + if (isExistingGame(game)) { + const updatedGame = await GameService.ChangeGame(game.id, formData); + if (updatedGame) { + changeGame(updatedGame); + reset({}, { keepValues: true }); + } + } else { + const addedGame = await GameService.AddGame(formData); + if (addedGame) { + changeGame(addedGame); + reset({}, { keepValues: true }); + } + } }; - const [cover, setCover] = useState(game.cover); + const onCoverDrop = useCallback( + (acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + const fileReader = new FileReader(); + fileReader.onload = async () => { + const coverName = await FilesService.UploadCover(file); + if (coverName) { + setValue("cover", coverName, { + shouldValidate: true, + shouldDirty: true, + }); + } + }; + fileReader.readAsDataURL(file); + }, + [setValue] + ); - const onDrop = useCallback((acceptedFiles: File[]) => { - const file = acceptedFiles[0]; - const fileReader = new FileReader(); - fileReader.onload = async () => { - const coverName = await FilesService.UploadCover(file); - if (coverName) { - setCover(coverName); - setValue("cover", coverName); - } - }; - fileReader.readAsDataURL(file); - }, []); + const { + getRootProps: getCoverDropRootProps, + getInputProps: getCoverDropInputProps, + isDragActive: isCoverDragActive, + } = useDropzone({ onDrop: onCoverDrop }); - const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop }); + const onTorrentDrop = useCallback( + (acceptedFiles: File[]) => { + const file = acceptedFiles[0]; + const fileReader = new FileReader(); + fileReader.onload = async () => { + const torrentName = await FilesService.UploadTorrent(file); + if (torrentName) { + setValue("torrent_file", torrentName, { + shouldValidate: true, + shouldDirty: true, + }); + } + }; + fileReader.readAsDataURL(file); + }, + [setValue] + ); - const formRef = useRef(null); + useEffect(() => console.log(errors), [errors]); + + const { + getRootProps: getTorrentDropRootProps, + getInputProps: getTorrentDropInputProps, + isDragActive: isTorrentDragActive, + } = useDropzone({ onDrop: onTorrentDrop }); + + const [trailer, setTrailer] = useState(game.trailer); return (
- {cover && ( -
- + {(watchedData.cover || editable) && ( +
+ {watchedData.cover && ( + + )} + {!watchedData.cover && editable && ( +
+ )} + {editable && ( - <> - - - {isDragActive ? ( -

Изменить обложку...

+
+ + + {isCoverDragActive ? ( +

Изменить обложку...

) : ( - - Для редактирования нажмите или перетащите новую обложку - поверх старой - + <> + + Для редактирования нажмите или перетащите новую обложку + поверх старой + + + Для редактирования нажмите на обложку и выберите фото + + )}
- +
)}
)} - - + + + + Введите название +

{ shouldValidate: true, shouldDirty: true, }); - console.log(); }} > {game.title}

- - {savedTimeout && ( - <> - - Редактируется - - )} - {!savedTimeout && "Сохранено"} - -
- {game.description && ( -
{ - setValue("description", e.currentTarget.innerText, { - shouldValidate: true, - shouldDirty: true, - }); - }} - > - {game.description} -
+ {editable && ( + + {savedTimeout && Object.keys(errors).length === 0 && ( + <> + + Редактируется + + )} + {savedTimeout && Object.keys(errors).length > 0 && ( + Некорректные данные + )} + {!savedTimeout && "Сохранено"} + + )} +
+
+ {errors.title?.message} +
+ {(game.description || editable) && ( + + + Введите описание + +
{ + setValue("description", e.currentTarget.innerText, { + shouldValidate: true, + shouldDirty: true, + }); + }} + > + {game.description} +
+
)}
{ { name: "Дата обновления раздачи", key: "update_date", - value: game.update_date.toLocaleDateString("ru-ru"), + value: isExistingGame(game) + ? game.update_date.toLocaleDateString("ru-ru") + : new Date().toLocaleDateString("ru-ru"), + edit: false, }, { name: "Язык", key: "language" }, { name: "Разработчик", key: "developer" }, { name: "Год выхода", key: "release_date", - value: game.release_date.toLocaleDateString("en-us", { - year: "numeric", - }), }, { name: "Объём загрузки", key: "download_size" }, ], - ] as { name: string; key: keyof GameCreateType; value?: string }[][] + ] as { + name: string; + key: keyof GameCreateType; + value?: string; + edit?: boolean; + }[][] ).map((section, i) => (
    {section.map((req) => ( -
  • - {req.name + ": "} - { - if (e.target.value === "") { - e.target.value = propertyUnknownText; - } - }} - > +
  • + {req.name + ": "} + + + Не известно + + { + setValue(req.key, e.currentTarget.innerText, { + shouldValidate: true, + shouldDirty: true, + }); + }} + > + {req.value ?? (game[req.key] as string)} + +
  • ))}
))}
- {game.trailer && getYouTubeID(game.trailer) && ( -