mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Work on game edit
This commit is contained in:
@@ -35,32 +35,30 @@ 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 ("username" | "password")[]).map(
|
{(["username", "password"] as (keyof LoginForm)[]).map((field) => (
|
||||||
(field) => (
|
<label
|
||||||
<label
|
className="flex flex-col items-start relative w-64 py-1"
|
||||||
className="flex flex-col items-start relative w-64 py-1"
|
key={field}
|
||||||
key={field}
|
>
|
||||||
>
|
<input
|
||||||
<input
|
{...register(field)}
|
||||||
{...register(field)}
|
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
||||||
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
placeholder=" "
|
||||||
placeholder=" "
|
autoComplete="off"
|
||||||
autoComplete="off"
|
/>
|
||||||
/>
|
<span
|
||||||
<span
|
className="peer-focus/search:opacity-0
|
||||||
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>
|
||||||
)
|
))}
|
||||||
)}
|
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
import { GameService } from "@/entities/game";
|
import { GameService } from "@/entities/game";
|
||||||
import { GameCard } from "@/features/gameCard";
|
import { GameCard } from "@/features/gameCard";
|
||||||
import { getYouTubeID } from "@/shared/utils";
|
import { GameInfo } from "@/widgets/gameInfo";
|
||||||
import { Section } from "@/widgets/section";
|
import { Section } from "@/widgets/section";
|
||||||
import clsx from "clsx";
|
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
export default async function Games({
|
export default async function Games({
|
||||||
params: { game_id },
|
params: { game_id },
|
||||||
@@ -15,104 +12,7 @@ export default async function Games({
|
|||||||
const game = await GameService.getGame(game_id);
|
const game = await GameService.getGame(game_id);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{game && (
|
{game && <GameInfo game={game} />}
|
||||||
<div className="p-4 flex flex-col lp:block">
|
|
||||||
{game.cover && (
|
|
||||||
<div className="lp:w-[60%] lp:px-4 lp:pl-0 py-2 float-left">
|
|
||||||
<Image
|
|
||||||
src={game.cover}
|
|
||||||
className="rounded-lg w-full object-contain"
|
|
||||||
alt=""
|
|
||||||
width={1280}
|
|
||||||
height={720}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<span className="lp:max-w-[40%]">
|
|
||||||
<h1 className="text-4xl">{game.title}</h1>
|
|
||||||
{game.description && (
|
|
||||||
<p className="text-md text-justify text-fg4 pt-2">
|
|
||||||
{game.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<div className="w-full flex justify-between pt-4">
|
|
||||||
{[
|
|
||||||
[
|
|
||||||
{ name: "Система", value: game.system },
|
|
||||||
{ name: "Процессор", value: game.processor },
|
|
||||||
{ name: "Оперативная память", value: game.memory },
|
|
||||||
{ name: "Видеокарта", value: game.graphics },
|
|
||||||
{ name: "Место на диске", value: game.storage },
|
|
||||||
],
|
|
||||||
[
|
|
||||||
{
|
|
||||||
name: "Версия игры",
|
|
||||||
value: `${
|
|
||||||
game.version
|
|
||||||
} (обновлена ${game.update_date.toLocaleDateString(
|
|
||||||
"ru-ru"
|
|
||||||
)})`,
|
|
||||||
},
|
|
||||||
{ name: "Язык", value: game.language },
|
|
||||||
{ name: "Разработчик", value: game.developer },
|
|
||||||
{
|
|
||||||
name: "Год выхода",
|
|
||||||
value: game.release_date.toLocaleDateString("en-us", {
|
|
||||||
year: "numeric",
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
{ name: "Объём загрузки", value: game.download_size },
|
|
||||||
],
|
|
||||||
].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="font-bold text-sm lp:text-md py-1"
|
|
||||||
>
|
|
||||||
{req.name + ": "}
|
|
||||||
<span
|
|
||||||
className={clsx(
|
|
||||||
"font-normal",
|
|
||||||
req.value === undefined && "text-fg4"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{req.value ?? "Не известно"}
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
{game.trailer && getYouTubeID(game.trailer) && (
|
|
||||||
<iframe
|
|
||||||
src={"https://youtube.com/embed/" + getYouTubeID(game.trailer)}
|
|
||||||
className="w-full aspect-video rounded-lg mt-4"
|
|
||||||
allowFullScreen
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<div className="relative w-full flex items-center justify-around pt-4">
|
|
||||||
<Link
|
|
||||||
href={
|
|
||||||
process.env.NEXT_PUBLIC_CONTENT_URL + "/" + game.torrent_file
|
|
||||||
}
|
|
||||||
className="p-4 bg-ac0 text-fg1 text-2xl rounded-lg"
|
|
||||||
>
|
|
||||||
Скачать {game.title}
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex justify-end">
|
|
||||||
<Link
|
|
||||||
className="text-right text-sm relative top-4 lp:-top-4"
|
|
||||||
href="/how_to_download"
|
|
||||||
>
|
|
||||||
Как скачать игру
|
|
||||||
<br /> с помощью .torrent файла?
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{gameCards && (
|
{gameCards && (
|
||||||
<Section
|
<Section
|
||||||
|
|||||||
@@ -52,8 +52,14 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--color-fg1);
|
color: var(--color-fg1);
|
||||||
background-color: var(--color-bg0);
|
background-color: var(--color-bg0);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
body * {
|
||||||
|
scrollbar-color: var(--color-ac0) var(--color-bg1);
|
||||||
|
scrollbar-width: thin;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1024px) {
|
@media (max-width: 1024px) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -19,7 +19,10 @@ export default function RootLayout({
|
|||||||
<ThemeProvider enableSystem={false} defaultTheme="light">
|
<ThemeProvider enableSystem={false} defaultTheme="light">
|
||||||
{auth}
|
{auth}
|
||||||
<Header />
|
<Header />
|
||||||
<div className="w-full h-full max-w-[var(--app-width)] m-auto overflow-y-auto">
|
<div
|
||||||
|
className="w-full h-[calc(100%_-_5rem)] \
|
||||||
|
max-w-[var(--app-width)] m-auto overflow-y-auto"
|
||||||
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
|
|||||||
@@ -1,15 +1,16 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
import { gameCardsSchema, GameCardType } from "./schemas/gameCard";
|
import { gameCardsSchema, GameCardType } from "./schemas/gameCard";
|
||||||
import { gameSchema, GameType } from "./schemas/game";
|
import { GameCreateType, gameSchema } from "./schemas/game";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
export abstract class GameService {
|
export abstract class GameService {
|
||||||
public static async getGameCards() {
|
public static async getGameCards() {
|
||||||
return await HTTPService.get<GameCardType[]>(
|
return await HTTPService.get("/games/cards", gameCardsSchema);
|
||||||
"/games/cards",
|
|
||||||
gameCardsSchema
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
public static async getGame(id: number) {
|
public static async getGame(id: number) {
|
||||||
return await HTTPService.get<GameType>(`/games/${id}`, gameSchema);
|
return await HTTPService.get(`/games/${id}`, gameSchema);
|
||||||
|
}
|
||||||
|
public static async changeGame(id: number, gameInfo: GameCreateType) {
|
||||||
|
return await HTTPService.put(`/games/${id}`, gameSchema, gameInfo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
import { GameCardType } from "./schemas/gameCard";
|
import { GameCardType } from "./schemas/gameCard";
|
||||||
import { GameType } from "./schemas/game";
|
import { GameType, GameCreateType, gameCreateSchema } from "./schemas/game";
|
||||||
import { GameService } from "./game";
|
import { GameService } from "./game";
|
||||||
|
|
||||||
export { GameService, type GameType, type GameCardType };
|
export {
|
||||||
|
GameService,
|
||||||
|
gameCreateSchema,
|
||||||
|
type GameType,
|
||||||
|
type GameCreateType,
|
||||||
|
type GameCardType,
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,46 +1,56 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { gameCardSchema } from "./gameCard";
|
import { gameCardBaseSchema } from "./gameCard";
|
||||||
|
|
||||||
export const gameSchema = gameCardSchema.and(
|
export const gameBaseSchema = gameCardBaseSchema.and(
|
||||||
z.object({
|
z.object({
|
||||||
torrent_file: z.string().min(1),
|
torrent_file: z.string().min(1),
|
||||||
trailer: z.string().optional(),
|
trailer: z.string().optional(),
|
||||||
|
|
||||||
system: z.string().optional(),
|
system: z.string().optional(),
|
||||||
processor: z.string().optional(),
|
processor: z.string().optional(),
|
||||||
memory: z.string().optional(),
|
memory: z.string().optional(),
|
||||||
graphics: z.string().optional(),
|
graphics: z.string().optional(),
|
||||||
storage: z.string().optional(),
|
storage: z.string().optional(),
|
||||||
|
|
||||||
developer: z.string().optional(),
|
developer: z.string().optional(),
|
||||||
language: z.string().optional(),
|
language: z.string().optional(),
|
||||||
download_size: z.string().optional(),
|
download_size: z.string().optional(),
|
||||||
|
|
||||||
update_date: z
|
release_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
upload_date: z
|
})
|
||||||
.string()
|
);
|
||||||
.min(1)
|
|
||||||
.transform((d) => new Date(d)),
|
export const gameCreateSchema = gameBaseSchema.and(z.object({}));
|
||||||
release_date: z
|
export type GameCreateType = z.infer<typeof gameCreateSchema>;
|
||||||
.string()
|
|
||||||
.min(1)
|
export const gameSchema = gameBaseSchema.and(
|
||||||
.transform((d) => new Date(d)),
|
z.object({
|
||||||
})
|
id: z.number().positive(),
|
||||||
|
owner_id: z.number().positive(),
|
||||||
|
update_date: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.transform((d) => new Date(d)),
|
||||||
|
upload_date: z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.transform((d) => new Date(d)),
|
||||||
|
})
|
||||||
);
|
);
|
||||||
export type GameType = z.infer<typeof gameSchema>;
|
export type GameType = z.infer<typeof gameSchema>;
|
||||||
|
|
||||||
export const isGame = (a: any): a is GameType => {
|
export const isGame = (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 (isGame(e)) games.push(gameSchema.parse(e));
|
||||||
else console.error("Game parse error - ", e);
|
else console.error("Game parse error - ", e);
|
||||||
});
|
});
|
||||||
return games;
|
return games;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const gameCardSchema = z
|
export const gameCardBaseSchema = z
|
||||||
.object({
|
.object({
|
||||||
id: z.number().positive(),
|
|
||||||
title: z.string().min(3),
|
title: z.string().min(3),
|
||||||
cover: z.string().optional(),
|
cover: z.string().optional(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
@@ -19,6 +18,12 @@ export const gameCardSchema = z
|
|||||||
: undefined,
|
: undefined,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const gameCardSchema = gameCardBaseSchema.and(
|
||||||
|
z.object({
|
||||||
|
id: z.number().positive(),
|
||||||
|
})
|
||||||
|
);
|
||||||
export type GameCardType = z.infer<typeof gameCardSchema>;
|
export type GameCardType = z.infer<typeof gameCardSchema>;
|
||||||
|
|
||||||
export const isGameCard = (a: any): a is GameCardType => {
|
export const isGameCard = (a: any): a is GameCardType => {
|
||||||
|
|||||||
@@ -11,10 +11,10 @@ import Cookies from "js-cookie";
|
|||||||
|
|
||||||
export abstract class UserService {
|
export abstract class UserService {
|
||||||
public static async Login(loginForm: LoginForm) {
|
public static async Login(loginForm: LoginForm) {
|
||||||
const accessToken = await HTTPService.post<TokenResponse>(
|
const accessToken = await HTTPService.post(
|
||||||
"/auth",
|
"/auth",
|
||||||
new URLSearchParams(Object.entries(loginForm)),
|
|
||||||
tokenResponseSchema,
|
tokenResponseSchema,
|
||||||
|
new URLSearchParams(Object.entries(loginForm)),
|
||||||
{
|
{
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
}
|
}
|
||||||
@@ -31,6 +31,10 @@ export abstract class UserService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static GetToken(): string | undefined {
|
||||||
|
return Cookies.get("access-token");
|
||||||
|
}
|
||||||
|
|
||||||
public static IdentifyYourself(): TokenData | undefined {
|
public static IdentifyYourself(): TokenData | undefined {
|
||||||
const token = Cookies.get("access-token");
|
const token = Cookies.get("access-token");
|
||||||
if (token) {
|
if (token) {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ export const GameCard = ({ card }: { card: GameCardType }) => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="flex items-center justify-between pr-2">
|
<div className="flex items-center justify-between pr-2">
|
||||||
<h2 className="text-3xl tb:text-xl py-1 group-hover/gamecard:underline underline-offset-4">
|
<h2 className="text-3xl tb:text-xl py-1 group-hover/gamecard:underline underline-offset-1">
|
||||||
{card.title}
|
{card.title}
|
||||||
</h2>
|
</h2>
|
||||||
{card.version && (
|
{card.version && (
|
||||||
|
|||||||
@@ -4,26 +4,86 @@ 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 from "swr";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
export const UserActivities = () => {
|
export const UserActivities = () => {
|
||||||
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
||||||
|
const [open, changeMenuOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PersonIcon className="mr-1 h-4 w-4" />
|
<span className="group/login cursor-pointer flex items-center">
|
||||||
{me && (
|
<PersonIcon className="mr-1 h-4 w-4" />
|
||||||
<span className="group/login cursor-pointer flex items-center">
|
{me && (
|
||||||
<span className="group-hover/login:underline">{me.name}</span>
|
<div className="relative">
|
||||||
</span>
|
<button
|
||||||
)}
|
className="group-hover/login:underline"
|
||||||
{!me && (
|
onClick={() => changeMenuOpen(!open)}
|
||||||
<Link
|
onBlur={() => changeMenuOpen(false)}
|
||||||
href="/login"
|
>
|
||||||
className="group/login cursor-pointer flex items-center"
|
{me.name}
|
||||||
>
|
</button>
|
||||||
<span className="group-hover/login:underline">Войти</span>
|
<li
|
||||||
</Link>
|
className={clsx(
|
||||||
)}
|
"h-0 absolute transition-all duration-300",
|
||||||
|
"overflow-hidden bg-bg4 rounded-lg pl-2",
|
||||||
|
"flex flex-col z-10 shadow-3xl w-60",
|
||||||
|
"right-0 top-8",
|
||||||
|
open && "h-40 py-2"
|
||||||
|
)}
|
||||||
|
onClick={() => changeMenuOpen(false)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
group: "Добавить:",
|
||||||
|
items: [
|
||||||
|
{ name: "Добавить игру", link: "/games/add" },
|
||||||
|
{ name: "Добавить фильм", link: "/films/add" },
|
||||||
|
{ name: "Добавить аудиокнигу", link: "/audiobooks/add" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{ name: "Выйти", link: "/logout" },
|
||||||
|
].map((item) => (
|
||||||
|
<ul key={item.group ?? item.name}>
|
||||||
|
{item.group && (
|
||||||
|
<>
|
||||||
|
<div className="text-xl font-bold">{item.group}</div>
|
||||||
|
<li className="pl-4 pb-0">
|
||||||
|
{item.items.map((item) => (
|
||||||
|
<ul key={item.name}>
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
className="text-lg py-2 cursor-pointer hover:underline"
|
||||||
|
href={item.link}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{!item.group && item.link && (
|
||||||
|
<Link
|
||||||
|
key={item.name}
|
||||||
|
className="text-xl font-bold py-2 cursor-pointer hover:underline"
|
||||||
|
href={item.link}
|
||||||
|
>
|
||||||
|
{item.name}
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
</li>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!me && (
|
||||||
|
<Link href="/login" className="cursor-pointer flex items-center">
|
||||||
|
<span className="group-hover/login:underline">Войти</span>
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,15 +1,25 @@
|
|||||||
|
import { UserService } from "@/entities/user";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
|
type Body = BodyInit | object;
|
||||||
|
|
||||||
export abstract class HTTPService {
|
export abstract class HTTPService {
|
||||||
public static async get<Z>(
|
public static async request<Z extends z.ZodTypeAny>(
|
||||||
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
url: string,
|
url: string,
|
||||||
schema: z.ZodTypeAny
|
schema: Z,
|
||||||
): Promise<Z | null> {
|
body?: Body,
|
||||||
|
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: "GET",
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
|
Authorization: "Bearer " + UserService.GetToken(),
|
||||||
|
...headers,
|
||||||
},
|
},
|
||||||
|
body: stringify ? JSON.stringify(body) : (body as BodyInit),
|
||||||
cache: "no-cache",
|
cache: "no-cache",
|
||||||
})
|
})
|
||||||
.then((r) => {
|
.then((r) => {
|
||||||
@@ -17,37 +27,34 @@ export abstract class HTTPService {
|
|||||||
else throw Error("Response ok = false");
|
else throw Error("Response ok = false");
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => schema.parse(d))
|
.then((d) => schema.parse(d) as z.infer<Z>)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return null;
|
return null;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async post<Z>(
|
public static async get<Z extends z.ZodTypeAny>(url: string, schema: Z) {
|
||||||
|
return await this.request<Z>("GET", url, schema);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static async post<Z extends z.ZodTypeAny>(
|
||||||
url: string,
|
url: string,
|
||||||
body: BodyInit,
|
schema: Z,
|
||||||
schema: z.ZodTypeAny,
|
body?: Body,
|
||||||
headers?: HeadersInit
|
headers?: HeadersInit,
|
||||||
): Promise<Z | null> {
|
stringify?: boolean
|
||||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
) {
|
||||||
method: "POST",
|
return await this.request<Z>("POST", url, schema, body, headers, stringify);
|
||||||
headers: {
|
}
|
||||||
accept: "application/json",
|
|
||||||
...headers,
|
public static async put<Z extends z.ZodType>(
|
||||||
},
|
url: string,
|
||||||
body: body,
|
schema: Z,
|
||||||
cache: "no-cache",
|
body?: Body,
|
||||||
})
|
headers?: HeadersInit,
|
||||||
.then((r) => {
|
stringify?: boolean
|
||||||
if (r && r.ok) return r;
|
) {
|
||||||
else throw Error("Response ok = false");
|
return await this.request<Z>("PUT", url, schema, body, headers, stringify);
|
||||||
})
|
|
||||||
.then((r) => r.json())
|
|
||||||
.then((d) => schema.parse(d))
|
|
||||||
.catch((e) => {
|
|
||||||
console.error(e);
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
184
src/widgets/gameInfo/gameInfo.tsx
Normal file
184
src/widgets/gameInfo/gameInfo.tsx
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import {
|
||||||
|
gameCreateSchema,
|
||||||
|
GameCreateType,
|
||||||
|
GameService,
|
||||||
|
GameType,
|
||||||
|
} from "@/entities/game";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { getYouTubeID } from "@/shared/utils";
|
||||||
|
import { UserService } from "@/entities/user";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { SubmitHandler, useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
|
||||||
|
export const GameInfo = ({ game }: { game: GameType }) => {
|
||||||
|
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
||||||
|
const [editable, setEditable] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => setEditable(me?.id === game.owner_id), [me, game]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
register,
|
||||||
|
handleSubmit,
|
||||||
|
setValue,
|
||||||
|
formState: { errors },
|
||||||
|
} = useForm<GameCreateType>({
|
||||||
|
resolver: zodResolver(gameCreateSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
register("torrent_file", { value: game.torrent_file });
|
||||||
|
register("cover", { value: game.cover });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(errors);
|
||||||
|
}, [errors]);
|
||||||
|
|
||||||
|
const onSubmit = (formData: GameCreateType) => {
|
||||||
|
const updatedGame = GameService.changeGame(game.id, formData);
|
||||||
|
console.log(updatedGame);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
|
className="p-4 flex flex-col lp:block"
|
||||||
|
>
|
||||||
|
{game.cover && (
|
||||||
|
<div className="lp:w-[60%] lp:px-4 lp:pl-0 py-2 float-left">
|
||||||
|
<Image
|
||||||
|
src={game.cover}
|
||||||
|
className="rounded-lg w-full object-contain"
|
||||||
|
alt=""
|
||||||
|
width={1280}
|
||||||
|
height={720}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<span className="lp:max-w-[40%]">
|
||||||
|
<h1
|
||||||
|
className={clsx(
|
||||||
|
"text-4xl outline-none",
|
||||||
|
!editable && "cursor-default"
|
||||||
|
)}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
contentEditable={editable}
|
||||||
|
{...register("title", { value: game.title })}
|
||||||
|
onInput={(e) => {
|
||||||
|
setValue("title", e.currentTarget.innerText, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
console.log();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{game.title}
|
||||||
|
</h1>
|
||||||
|
{game.description && (
|
||||||
|
<div
|
||||||
|
contentEditable={editable}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
className={clsx(
|
||||||
|
"text-md text-justify",
|
||||||
|
"text-fg4 pt-2 outline-none",
|
||||||
|
!editable && "cursor-default"
|
||||||
|
)}
|
||||||
|
{...register("description", { value: game.description })}
|
||||||
|
onInput={(e) => {
|
||||||
|
setValue("description", e.currentTarget.innerText, {
|
||||||
|
shouldValidate: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{game.description}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
className={clsx(
|
||||||
|
"w-full flex justify-between pt-4",
|
||||||
|
!editable && "cursor-default"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
[
|
||||||
|
{ name: "Система", key: "system" },
|
||||||
|
{ name: "Процессор", key: "processor" },
|
||||||
|
{ name: "Оперативная память", key: "memory" },
|
||||||
|
{ name: "Видеокарта", key: "graphics" },
|
||||||
|
{ name: "Место на диске", key: "storage" },
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{ name: "Версия игры", key: "version" },
|
||||||
|
{
|
||||||
|
name: "Дата обновления раздачи",
|
||||||
|
key: "update_date",
|
||||||
|
value: game.update_date.toLocaleDateString("ru-ru"),
|
||||||
|
},
|
||||||
|
{ name: "Язык", key: "language" },
|
||||||
|
{ name: "Разработчик", key: "developer" },
|
||||||
|
{
|
||||||
|
name: "Год выхода",
|
||||||
|
key: "release_date",
|
||||||
|
value: game.release_date.toLocaleDateString("en-us", {
|
||||||
|
year: "numeric",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{ name: "Объём загрузки", key: "download_size" },
|
||||||
|
],
|
||||||
|
].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="font-bold text-sm lp:text-md py-1">
|
||||||
|
{req.name + ": "}
|
||||||
|
<input
|
||||||
|
readOnly={!editable}
|
||||||
|
className={clsx(
|
||||||
|
"font-normal outline-none bg-bg1",
|
||||||
|
req.value === undefined && "text-fg4"
|
||||||
|
)}
|
||||||
|
{...register(req.key as keyof GameCreateType)}
|
||||||
|
defaultValue={
|
||||||
|
req.value ??
|
||||||
|
(game[req.key as keyof GameType] as string) ??
|
||||||
|
"Не известно"
|
||||||
|
}
|
||||||
|
></input>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{game.trailer && getYouTubeID(game.trailer) && (
|
||||||
|
<iframe
|
||||||
|
src={"https://youtube.com/embed/" + getYouTubeID(game.trailer)}
|
||||||
|
className="w-full aspect-video rounded-lg mt-4"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="relative w-full flex items-center justify-around pt-4">
|
||||||
|
<Link
|
||||||
|
href={process.env.NEXT_PUBLIC_CONTENT_URL + "/" + game.torrent_file}
|
||||||
|
className="p-4 bg-ac0 text-fg1 text-2xl rounded-lg"
|
||||||
|
>
|
||||||
|
Скачать {game.title}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<div className="w-full flex justify-end">
|
||||||
|
<Link
|
||||||
|
className="text-right text-sm relative top-4 lp:-top-4"
|
||||||
|
href="/how_to_download"
|
||||||
|
>
|
||||||
|
Как скачать игру
|
||||||
|
<br /> с помощью .torrent файла?
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<input type="submit" className=" m-2 p-2 bg-ac0" />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
3
src/widgets/gameInfo/index.ts
Normal file
3
src/widgets/gameInfo/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { GameInfo } from "./gameInfo";
|
||||||
|
|
||||||
|
export { GameInfo };
|
||||||
@@ -50,6 +50,7 @@ export const Header = () => {
|
|||||||
</span>
|
</span>
|
||||||
<label className="flex flex-col items-start relative w-36">
|
<label className="flex flex-col items-start relative w-36">
|
||||||
<input
|
<input
|
||||||
|
type="search"
|
||||||
className="peer/search w-full rounded-lg bg-bg4 px-2"
|
className="peer/search w-full rounded-lg bg-bg4 px-2"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const MobileMenu = ({
|
|||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
className="peer w-16 h-16 *:w-12 *:h-1 *:bg-fg1 *:my-3
|
className="w-16 h-16 *:w-12 *:h-1 *:bg-fg1 *:my-3
|
||||||
*:transition-all *:duration-300 *:relative"
|
*:transition-all *:duration-300 *:relative"
|
||||||
onClick={() => changeMenuOpen(!open)}
|
onClick={() => changeMenuOpen(!open)}
|
||||||
onBlur={() => changeMenuOpen(false)}
|
onBlur={() => changeMenuOpen(false)}
|
||||||
@@ -30,7 +30,7 @@ export const MobileMenu = ({
|
|||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"h-0 absolute transition-all duration-300 overflow-hidden\
|
"h-0 absolute transition-all duration-300 overflow-hidden\
|
||||||
bg-bg4 rounded-lg px-4 flex flex-col",
|
bg-bg4 rounded-lg px-4 flex flex-col shadow-xl",
|
||||||
open && "h-32"
|
open && "h-32"
|
||||||
)}
|
)}
|
||||||
onClick={() => changeMenuOpen(false)}
|
onClick={() => changeMenuOpen(false)}
|
||||||
|
|||||||
Reference in New Issue
Block a user