mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Work on game form
This commit is contained in:
41
package-lock.json
generated
41
package-lock.json
generated
@@ -17,6 +17,7 @@
|
|||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-hook-form": "^7.51.4",
|
||||||
"react-responsive-masonry": "^2.2.0",
|
"react-responsive-masonry": "^2.2.0",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
@@ -923,6 +924,14 @@
|
|||||||
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
"integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"node_modules/attr-accept": {
|
||||||
|
"version": "2.2.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
|
||||||
|
"integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=4"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/available-typed-arrays": {
|
"node_modules/available-typed-arrays": {
|
||||||
"version": "1.0.7",
|
"version": "1.0.7",
|
||||||
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
"resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
|
||||||
@@ -2026,6 +2035,17 @@
|
|||||||
"node": "^10.12.0 || >=12.0.0"
|
"node": "^10.12.0 || >=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/file-selector": {
|
||||||
|
"version": "0.6.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
|
||||||
|
"integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
|
||||||
|
"dependencies": {
|
||||||
|
"tslib": "^2.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 12"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -3263,7 +3283,6 @@
|
|||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
|
||||||
"dev": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
@@ -3725,7 +3744,6 @@
|
|||||||
"version": "15.8.1",
|
"version": "15.8.1",
|
||||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||||
"dev": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"loose-envify": "^1.4.0",
|
"loose-envify": "^1.4.0",
|
||||||
"object-assign": "^4.1.1",
|
"object-assign": "^4.1.1",
|
||||||
@@ -3784,6 +3802,22 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-dropzone": {
|
||||||
|
"version": "14.2.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
|
||||||
|
"integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
|
||||||
|
"dependencies": {
|
||||||
|
"attr-accept": "^2.2.2",
|
||||||
|
"file-selector": "^0.6.0",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 10.13"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.8 || 18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-hook-form": {
|
"node_modules/react-hook-form": {
|
||||||
"version": "7.51.4",
|
"version": "7.51.4",
|
||||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.51.4.tgz",
|
||||||
@@ -3802,8 +3836,7 @@
|
|||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
|
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"node_modules/react-responsive-masonry": {
|
"node_modules/react-responsive-masonry": {
|
||||||
"version": "2.2.0",
|
"version": "2.2.0",
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18",
|
"react-dom": "^18",
|
||||||
|
"react-dropzone": "^14.2.3",
|
||||||
"react-hook-form": "^7.51.4",
|
"react-hook-form": "^7.51.4",
|
||||||
"react-responsive-masonry": "^2.2.0",
|
"react-responsive-masonry": "^2.2.0",
|
||||||
"swr": "^2.2.5",
|
"swr": "^2.2.5",
|
||||||
|
|||||||
16
src/entities/files/files.ts
Normal file
16
src/entities/files/files.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
|
import { coverNameSchema } from "./schemas/cover";
|
||||||
|
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
3
src/entities/files/index.ts
Normal file
3
src/entities/files/index.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import { FilesService } from "./files";
|
||||||
|
|
||||||
|
export { FilesService };
|
||||||
4
src/entities/files/schemas/cover.ts
Normal file
4
src/entities/files/schemas/cover.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const coverNameSchema = z.string().min(5);
|
||||||
|
export type CoverNameType = z.infer<typeof coverNameSchema>;
|
||||||
@@ -1,56 +1,56 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { gameCardBaseSchema } from "./gameCard";
|
import { gameCardBaseSchema } from "./gameCard";
|
||||||
|
|
||||||
export const gameBaseSchema = gameCardBaseSchema.and(
|
export const gameBaseSchema = gameCardBaseSchema.merge(
|
||||||
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(),
|
||||||
|
|
||||||
release_date: z
|
release_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
export const gameCreateSchema = gameBaseSchema.and(z.object({}));
|
export const gameCreateSchema = gameBaseSchema.merge(z.object({}));
|
||||||
export type GameCreateType = z.infer<typeof gameCreateSchema>;
|
export type GameCreateType = z.infer<typeof gameCreateSchema>;
|
||||||
|
|
||||||
export const gameSchema = gameBaseSchema.and(
|
export const gameSchema = gameBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().positive(),
|
id: z.number().positive(),
|
||||||
owner_id: z.number().positive(),
|
owner_id: z.number().positive(),
|
||||||
update_date: z
|
update_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
upload_date: z
|
upload_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.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,40 +1,28 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const gameCardBaseSchema = z
|
export const gameCardBaseSchema = z.object({
|
||||||
.object({
|
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(),
|
version: z.string().optional(),
|
||||||
version: z.string().optional(),
|
});
|
||||||
})
|
|
||||||
.transform((card) => {
|
|
||||||
return {
|
|
||||||
...card,
|
|
||||||
cover: card.cover
|
|
||||||
? process.env.NEXT_PUBLIC_COVER_FULL_URL + "/" + card.cover
|
|
||||||
: undefined,
|
|
||||||
cover_preview: card.cover
|
|
||||||
? process.env.NEXT_PUBLIC_COVER_PREVIEW_URL + "/" + card.cover
|
|
||||||
: undefined,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
export const gameCardSchema = gameCardBaseSchema.and(
|
export const gameCardSchema = gameCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().positive(),
|
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 => {
|
||||||
return gameCardSchema.safeParse(a).success;
|
return gameCardSchema.safeParse(a).success;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gameCardsSchema = z.array(z.any()).transform((a) => {
|
export const gameCardsSchema = z.array(z.any()).transform((a) => {
|
||||||
const cards: GameCardType[] = [];
|
const cards: GameCardType[] = [];
|
||||||
a.forEach((e) => {
|
a.forEach((e) => {
|
||||||
if (isGameCard(e)) cards.push(gameCardSchema.parse(e));
|
if (isGameCard(e)) cards.push(gameCardSchema.parse(e));
|
||||||
else console.error("GameCard parse error - ", e);
|
else console.error("GameCard parse error - ", e);
|
||||||
});
|
});
|
||||||
return cards;
|
return cards;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const userSchema = z.object({
|
export const userSchema = z.object({
|
||||||
id: z.number().positive(),
|
id: z.number().positive(),
|
||||||
name: z.string().min(3),
|
username: z.string().min(3),
|
||||||
email: z.string().min(3),
|
email: z.string().min(3),
|
||||||
});
|
});
|
||||||
export type User = z.infer<typeof userSchema>;
|
export type User = z.infer<typeof userSchema>;
|
||||||
|
|||||||
@@ -1,52 +1,53 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
import {
|
import {
|
||||||
LoginForm,
|
LoginForm,
|
||||||
TokenData,
|
TokenData,
|
||||||
tokenDataSchema,
|
tokenDataSchema,
|
||||||
TokenResponse,
|
TokenResponse,
|
||||||
tokenResponseSchema,
|
tokenResponseSchema,
|
||||||
} from "./schemas/auth";
|
} from "./schemas/auth";
|
||||||
import { jwtDecode } from "jwt-decode";
|
import { jwtDecode } from "jwt-decode";
|
||||||
import Cookies from "js-cookie";
|
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(
|
const accessToken = await HTTPService.post(
|
||||||
"/auth",
|
"/auth",
|
||||||
tokenResponseSchema,
|
tokenResponseSchema,
|
||||||
new URLSearchParams(Object.entries(loginForm)),
|
new URLSearchParams(Object.entries(loginForm)),
|
||||||
{
|
{
|
||||||
"Content-Type": "application/x-www-form-urlencoded",
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
}
|
},
|
||||||
);
|
false
|
||||||
if (accessToken) {
|
);
|
||||||
const tokenData = this.DecodeToken(accessToken);
|
if (accessToken) {
|
||||||
if (tokenData) {
|
const tokenData = this.DecodeToken(accessToken);
|
||||||
Cookies.set("access-token", accessToken, {
|
if (tokenData) {
|
||||||
secure: true,
|
Cookies.set("access-token", accessToken, {
|
||||||
expires: tokenData.expire,
|
secure: true,
|
||||||
});
|
expires: tokenData.expire,
|
||||||
return tokenData;
|
});
|
||||||
}
|
return tokenData;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static GetToken(): string | undefined {
|
public static GetToken(): string | undefined {
|
||||||
return Cookies.get("access-token");
|
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) {
|
||||||
return this.DecodeToken(token);
|
return this.DecodeToken(token);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static DecodeToken(token: string): TokenData | undefined {
|
public static DecodeToken(token: string): TokenData | undefined {
|
||||||
const tokenPayload = jwtDecode(token);
|
const tokenPayload = jwtDecode(token);
|
||||||
const parseResult = tokenDataSchema.safeParse(tokenPayload);
|
const parseResult = tokenDataSchema.safeParse(tokenPayload);
|
||||||
if (parseResult.success) {
|
if (parseResult.success) {
|
||||||
return parseResult.data;
|
return parseResult.data;
|
||||||
} else console.error("JWT payload broken - " + parseResult.error);
|
} else console.error("JWT payload broken - " + parseResult.error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,32 @@
|
|||||||
import { GameCardType } from "@/entities/game";
|
import { GameCardType } from "@/entities/game";
|
||||||
import Image from "next/image";
|
import { Img } from "@/shared/ui";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
|
||||||
export const GameCard = ({ card }: { card: GameCardType }) => {
|
export const GameCard = ({ card }: { card: GameCardType }) => {
|
||||||
return (
|
return (
|
||||||
<Link className="group/gamecard cursor-pointer" href={"/games/" + card.id}>
|
<Link className="group/gamecard cursor-pointer" href={"/games/" + card.id}>
|
||||||
{!!card.cover_preview && (
|
{!!card.cover && (
|
||||||
<Image
|
<Img
|
||||||
src={card.cover_preview}
|
src={card.cover}
|
||||||
className="rounded-lg object-contain"
|
preview={true}
|
||||||
alt=""
|
className="rounded-lg object-contain"
|
||||||
width={1280}
|
width={1280}
|
||||||
height={720}
|
height={720}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<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-1">
|
<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 && (
|
||||||
<span className="text-xs max-w-[30%] text-right line-clamp-2 text-fg4">
|
<span className="text-xs max-w-[30%] text-right line-clamp-2 text-fg4">
|
||||||
{card.version}
|
{card.version}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||||
{card.description}
|
{card.description}
|
||||||
</p>
|
</p>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,84 +6,85 @@ import Link from "next/link";
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
import { TokenData } from "@/entities/user/schemas/auth";
|
||||||
|
|
||||||
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);
|
const [open, changeMenuOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<span className="group/login cursor-pointer flex items-center">
|
<span className="group/login cursor-pointer flex items-center">
|
||||||
<PersonIcon className="mr-1 h-4 w-4" />
|
<PersonIcon className="mr-1 h-4 w-4" />
|
||||||
{me && (
|
{me && (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
className="group-hover/login:underline"
|
className="group-hover/login:underline"
|
||||||
onClick={() => changeMenuOpen(!open)}
|
onClick={() => changeMenuOpen(!open)}
|
||||||
onBlur={() => changeMenuOpen(false)}
|
onBlur={() => changeMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{me.name}
|
{me.username}
|
||||||
</button>
|
</button>
|
||||||
<li
|
<li
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"h-0 absolute transition-all duration-300",
|
"h-0 absolute transition-all duration-300",
|
||||||
"overflow-hidden bg-bg4 rounded-lg pl-2",
|
"overflow-hidden bg-bg4 rounded-lg pl-2",
|
||||||
"flex flex-col z-10 shadow-3xl w-60",
|
"flex flex-col z-10 shadow-3xl w-60",
|
||||||
"right-0 top-8",
|
"right-0 top-8",
|
||||||
open && "h-40 py-2"
|
open && "h-40 py-2"
|
||||||
)}
|
)}
|
||||||
onClick={() => changeMenuOpen(false)}
|
onClick={() => changeMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{[
|
{[
|
||||||
{
|
{
|
||||||
group: "Добавить:",
|
group: "Добавить:",
|
||||||
items: [
|
items: [
|
||||||
{ name: "Добавить игру", link: "/games/add" },
|
{ name: "Добавить игру", link: "/games/add" },
|
||||||
{ name: "Добавить фильм", link: "/films/add" },
|
{ name: "Добавить фильм", link: "/films/add" },
|
||||||
{ name: "Добавить аудиокнигу", link: "/audiobooks/add" },
|
{ name: "Добавить аудиокнигу", link: "/audiobooks/add" },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{ name: "Выйти", link: "/logout" },
|
{ name: "Выйти", link: "/logout" },
|
||||||
].map((item) => (
|
].map((item) => (
|
||||||
<ul key={item.group ?? item.name}>
|
<ul key={item.group ?? item.name}>
|
||||||
{item.group && (
|
{item.group && (
|
||||||
<>
|
<>
|
||||||
<div className="text-xl font-bold">{item.group}</div>
|
<div className="text-xl font-bold">{item.group}</div>
|
||||||
<li className="pl-4 pb-0">
|
<li className="pl-4 pb-0">
|
||||||
{item.items.map((item) => (
|
{item.items.map((item) => (
|
||||||
<ul key={item.name}>
|
<ul key={item.name}>
|
||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
className="text-lg py-2 cursor-pointer hover:underline"
|
className="text-lg py-2 cursor-pointer hover:underline"
|
||||||
href={item.link}
|
href={item.link}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
</ul>
|
</ul>
|
||||||
))}
|
))}
|
||||||
</li>
|
</li>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!item.group && item.link && (
|
{!item.group && item.link && (
|
||||||
<Link
|
<Link
|
||||||
key={item.name}
|
key={item.name}
|
||||||
className="text-xl font-bold py-2 cursor-pointer hover:underline"
|
className="text-xl font-bold py-2 cursor-pointer hover:underline"
|
||||||
href={item.link}
|
href={item.link}
|
||||||
>
|
>
|
||||||
{item.name}
|
{item.name}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
))}
|
))}
|
||||||
</li>
|
</li>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!me && (
|
{!me && (
|
||||||
<Link href="/login" className="cursor-pointer flex items-center">
|
<Link href="/login" className="cursor-pointer flex items-center">
|
||||||
<span className="group-hover/login:underline">Войти</span>
|
<span className="group-hover/login:underline">Войти</span>
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { SearchIcon } from "./searchIcon";
|
import { SearchIcon } from "./searchIcon";
|
||||||
import { PersonIcon } from "./personIcon";
|
import { PersonIcon } from "./personIcon";
|
||||||
import { SunIcon } from "./sunIcon";
|
import { SunIcon } from "./sunIcon";
|
||||||
|
import { SpinnerIcon } from "./spinnerIcon";
|
||||||
|
|
||||||
export { SearchIcon, PersonIcon, SunIcon };
|
export { SearchIcon, PersonIcon, SunIcon, SpinnerIcon };
|
||||||
|
|||||||
28
src/shared/assets/icons/spinnerIcon.tsx
Normal file
28
src/shared/assets/icons/spinnerIcon.tsx
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
export const SpinnerIcon = ({ className }: { className?: string }) => {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
className={"w-4 h-4 text-fg4 animate-spin " + className}
|
||||||
|
viewBox="0 0 64 64"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M32 3C35.8083 3 39.5794 3.75011 43.0978 5.20749C46.6163 6.66488 49.8132 8.80101 52.5061 11.4939C55.199 14.1868 57.3351 17.3837 58.7925 20.9022C60.2499 24.4206 61 28.1917 61 32C61 35.8083 60.2499 39.5794 58.7925 43.0978C57.3351 46.6163 55.199 49.8132 52.5061 52.5061C49.8132 55.199 46.6163 57.3351 43.0978 58.7925C39.5794 60.2499 35.8083 61 32 61C28.1917 61 24.4206 60.2499 20.9022 58.7925C17.3837 57.3351 14.1868 55.199 11.4939 52.5061C8.801 49.8132 6.66487 46.6163 5.20749 43.0978C3.7501 39.5794 3 35.8083 3 32C3 28.1917 3.75011 24.4206 5.2075 20.9022C6.66489 17.3837 8.80101 14.1868 11.4939 11.4939C14.1868 8.80099 17.3838 6.66487 20.9022 5.20749C24.4206 3.7501 28.1917 3 32 3L32 3Z"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
></path>
|
||||||
|
<path
|
||||||
|
d="M32 3C36.5778 3 41.0906 4.08374 45.1692 6.16256C49.2477 8.24138 52.7762 11.2562 55.466 14.9605C58.1558 18.6647 59.9304 22.9531 60.6448 27.4748C61.3591 31.9965 60.9928 36.6232 59.5759 40.9762"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth="5"
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
className="text-bg1"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
};
|
||||||
33
src/shared/ui/image.tsx
Normal file
33
src/shared/ui/image.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export const Img = ({
|
||||||
|
src,
|
||||||
|
preview = false,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
alt,
|
||||||
|
className,
|
||||||
|
}: {
|
||||||
|
src: string;
|
||||||
|
preview?: boolean;
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
alt?: string;
|
||||||
|
className: string;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Image
|
||||||
|
className={className}
|
||||||
|
src={
|
||||||
|
(preview
|
||||||
|
? process.env.NEXT_PUBLIC_COVER_PREVIEW_URL
|
||||||
|
: process.env.NEXT_PUBLIC_COVER_FULL_URL) +
|
||||||
|
"/" +
|
||||||
|
src
|
||||||
|
}
|
||||||
|
width={width}
|
||||||
|
height={height}
|
||||||
|
alt={alt ?? ""}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
import { Modal } from "./modal";
|
import { Modal } from "./modal";
|
||||||
|
import { Img } from "./image";
|
||||||
|
|
||||||
export { Modal };
|
export { Modal, Img };
|
||||||
|
|||||||
@@ -4,57 +4,61 @@ import { z } from "zod";
|
|||||||
type Body = BodyInit | object;
|
type Body = BodyInit | object;
|
||||||
|
|
||||||
export abstract class HTTPService {
|
export abstract class HTTPService {
|
||||||
public static async request<Z extends z.ZodTypeAny>(
|
public static async request<Z extends z.ZodTypeAny>(
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
body?: Body,
|
body?: Body,
|
||||||
headers?: HeadersInit,
|
headers?: HeadersInit,
|
||||||
stringify?: boolean
|
stringify?: boolean
|
||||||
) {
|
) {
|
||||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
accept: "application/json",
|
accept: "application/json",
|
||||||
Authorization: "Bearer " + UserService.GetToken(),
|
...((stringify ?? true) != true
|
||||||
...headers,
|
? {}
|
||||||
},
|
: { "Content-Type": "application/json" }),
|
||||||
body: stringify ? JSON.stringify(body) : (body as BodyInit),
|
Authorization: "Bearer " + UserService.GetToken(),
|
||||||
cache: "no-cache",
|
...headers,
|
||||||
})
|
},
|
||||||
.then((r) => {
|
body:
|
||||||
if (r && r.ok) return r;
|
(stringify ?? true) != true ? (body as BodyInit) : JSON.stringify(body),
|
||||||
else throw Error("Response ok = false");
|
cache: "no-cache",
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => {
|
||||||
.then((d) => schema.parse(d) as z.infer<Z>)
|
if (r && r.ok) return r;
|
||||||
.catch((e) => {
|
else throw Error("Response ok = false");
|
||||||
console.error(e);
|
})
|
||||||
return null;
|
.then((r) => r.json())
|
||||||
});
|
.then((d) => schema.parse(d) as z.infer<Z>)
|
||||||
}
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public static async get<Z extends z.ZodTypeAny>(url: string, schema: Z) {
|
public static async get<Z extends z.ZodTypeAny>(url: string, schema: Z) {
|
||||||
return await this.request<Z>("GET", url, schema);
|
return await this.request<Z>("GET", url, schema);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async post<Z extends z.ZodTypeAny>(
|
public static async post<Z extends z.ZodTypeAny>(
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
body?: Body,
|
body?: Body,
|
||||||
headers?: HeadersInit,
|
headers?: HeadersInit,
|
||||||
stringify?: boolean
|
stringify?: boolean
|
||||||
) {
|
) {
|
||||||
return await this.request<Z>("POST", url, schema, body, headers, stringify);
|
return await this.request<Z>("POST", url, schema, body, headers, stringify);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async put<Z extends z.ZodType>(
|
public static async put<Z extends z.ZodType>(
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
body?: Body,
|
body?: Body,
|
||||||
headers?: HeadersInit,
|
headers?: HeadersInit,
|
||||||
stringify?: boolean
|
stringify?: boolean
|
||||||
) {
|
) {
|
||||||
return await this.request<Z>("PUT", url, schema, body, headers, stringify);
|
return await this.request<Z>("PUT", url, schema, body, headers, stringify);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,184 +1,273 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
gameCreateSchema,
|
gameCreateSchema,
|
||||||
GameCreateType,
|
GameCreateType,
|
||||||
GameService,
|
GameService,
|
||||||
GameType,
|
GameType,
|
||||||
} from "@/entities/game";
|
} from "@/entities/game";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { getYouTubeID } from "@/shared/utils";
|
import { getYouTubeID } from "@/shared/utils";
|
||||||
import { UserService } from "@/entities/user";
|
import { UserService } from "@/entities/user";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useEffect, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { SubmitHandler, useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Img } from "@/shared/ui";
|
||||||
|
import { useDropzone } from "react-dropzone";
|
||||||
|
import { FilesService } from "@/entities/files";
|
||||||
|
import { SpinnerIcon } from "@/shared/assets/icons";
|
||||||
|
|
||||||
|
const propertyUnknownText = "Не известно";
|
||||||
|
|
||||||
export const GameInfo = ({ game }: { game: GameType }) => {
|
export const GameInfo = ({ game }: { game: GameType }) => {
|
||||||
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
||||||
const [editable, setEditable] = useState<boolean>(false);
|
const [editable, setEditable] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => setEditable(me?.id === game.owner_id), [me, game]);
|
useEffect(() => setEditable(me?.id === game.owner_id), [me, game]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
formState: { errors },
|
watch,
|
||||||
} = useForm<GameCreateType>({
|
formState: { dirtyFields, errors },
|
||||||
resolver: zodResolver(gameCreateSchema),
|
} = useForm<GameCreateType>({
|
||||||
});
|
resolver: zodResolver(gameCreateSchema),
|
||||||
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
register("torrent_file", { value: game.torrent_file });
|
register("torrent_file", { value: game.torrent_file });
|
||||||
register("cover", { value: game.cover });
|
register("cover", { value: game.cover });
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
const [savedTimeout, changeSavedTimeout] = useState<NodeJS.Timeout | null>(
|
||||||
console.log(errors);
|
null
|
||||||
}, [errors]);
|
);
|
||||||
|
const watchedData = watch();
|
||||||
|
const [formData, changeFormData] = useState<GameCreateType | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!Object.keys(dirtyFields).length) return;
|
||||||
|
if (JSON.stringify(watchedData) === JSON.stringify(formData)) return;
|
||||||
|
console.log(dirtyFields);
|
||||||
|
changeFormData(watchedData);
|
||||||
|
if (savedTimeout) clearTimeout(savedTimeout);
|
||||||
|
changeSavedTimeout(
|
||||||
|
setTimeout(() => {
|
||||||
|
if (formRef.current) formRef.current.requestSubmit();
|
||||||
|
}, 5000)
|
||||||
|
);
|
||||||
|
}, [watchedData]);
|
||||||
|
|
||||||
const onSubmit = (formData: GameCreateType) => {
|
const onSubmit = async (formData: GameCreateType) => {
|
||||||
const updatedGame = GameService.changeGame(game.id, formData);
|
changeSavedTimeout(null);
|
||||||
console.log(updatedGame);
|
const updatedGame = await GameService.changeGame(game.id, formData);
|
||||||
};
|
console.log(updatedGame);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
const [cover, setCover] = useState<string | undefined>(game.cover);
|
||||||
<form
|
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||||
className="p-4 flex flex-col lp:block"
|
const file = acceptedFiles[0];
|
||||||
>
|
const fileReader = new FileReader();
|
||||||
{game.cover && (
|
fileReader.onload = async () => {
|
||||||
<div className="lp:w-[60%] lp:px-4 lp:pl-0 py-2 float-left">
|
const coverName = await FilesService.UploadCover(file);
|
||||||
<Image
|
if (coverName) {
|
||||||
src={game.cover}
|
setCover(coverName);
|
||||||
className="rounded-lg w-full object-contain"
|
setValue("cover", coverName);
|
||||||
alt=""
|
}
|
||||||
width={1280}
|
};
|
||||||
height={720}
|
fileReader.readAsDataURL(file);
|
||||||
/>
|
}, []);
|
||||||
</div>
|
|
||||||
)}
|
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
||||||
<span className="lp:max-w-[40%]">
|
|
||||||
<h1
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
className={clsx(
|
|
||||||
"text-4xl outline-none",
|
return (
|
||||||
!editable && "cursor-default"
|
<form
|
||||||
)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
suppressContentEditableWarning={true}
|
className="p-4 flex flex-col lp:block"
|
||||||
contentEditable={editable}
|
ref={formRef}
|
||||||
{...register("title", { value: game.title })}
|
>
|
||||||
onInput={(e) => {
|
{cover && (
|
||||||
setValue("title", e.currentTarget.innerText, {
|
<div
|
||||||
shouldValidate: true,
|
className="lp:w-[60%] lp:px-4 lp:pl-0 pt-2 float-left"
|
||||||
});
|
{...(editable ? getRootProps() : {})}
|
||||||
console.log();
|
>
|
||||||
}}
|
<Img
|
||||||
>
|
src={cover}
|
||||||
{game.title}
|
preview={false}
|
||||||
</h1>
|
className="transition-all rounded-lg w-full object-contain"
|
||||||
{game.description && (
|
width={1280}
|
||||||
<div
|
height={720}
|
||||||
contentEditable={editable}
|
/>
|
||||||
suppressContentEditableWarning={true}
|
{editable && (
|
||||||
className={clsx(
|
<>
|
||||||
"text-md text-justify",
|
<input {...getInputProps()} />
|
||||||
"text-fg4 pt-2 outline-none",
|
<span className="flex items-center ju w-full p-1">
|
||||||
!editable && "cursor-default"
|
{isDragActive ? (
|
||||||
)}
|
<p>Изменить обложку...</p>
|
||||||
{...register("description", { value: game.description })}
|
) : (
|
||||||
onInput={(e) => {
|
<span className="text-sm w-full flex justify-around">
|
||||||
setValue("description", e.currentTarget.innerText, {
|
Для редактирования нажмите или перетащите новую обложку
|
||||||
shouldValidate: true,
|
поверх старой
|
||||||
});
|
</span>
|
||||||
}}
|
)}
|
||||||
>
|
</span>
|
||||||
{game.description}
|
</>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</span>
|
)}
|
||||||
<div
|
<span className="lp:max-w-[40%]">
|
||||||
className={clsx(
|
<span className="flex items-end justify-between">
|
||||||
"w-full flex justify-between pt-4",
|
<h1
|
||||||
!editable && "cursor-default"
|
className={clsx(
|
||||||
)}
|
"text-4xl outline-none max-w-[80%]",
|
||||||
>
|
!editable && "cursor-default"
|
||||||
{[
|
)}
|
||||||
[
|
suppressContentEditableWarning={true}
|
||||||
{ name: "Система", key: "system" },
|
contentEditable={editable}
|
||||||
{ name: "Процессор", key: "processor" },
|
{...register("title", { value: game.title })}
|
||||||
{ name: "Оперативная память", key: "memory" },
|
onInput={(e) => {
|
||||||
{ name: "Видеокарта", key: "graphics" },
|
setValue("title", e.currentTarget.innerText, {
|
||||||
{ name: "Место на диске", key: "storage" },
|
shouldValidate: true,
|
||||||
],
|
shouldDirty: true,
|
||||||
[
|
});
|
||||||
{ name: "Версия игры", key: "version" },
|
console.log();
|
||||||
{
|
}}
|
||||||
name: "Дата обновления раздачи",
|
>
|
||||||
key: "update_date",
|
{game.title}
|
||||||
value: game.update_date.toLocaleDateString("ru-ru"),
|
</h1>
|
||||||
},
|
<span className="text-sm text-fg4 flex items-center">
|
||||||
{ name: "Язык", key: "language" },
|
{savedTimeout && (
|
||||||
{ name: "Разработчик", key: "developer" },
|
<>
|
||||||
{
|
<SpinnerIcon className="mr-2" />
|
||||||
name: "Год выхода",
|
Редактируется
|
||||||
key: "release_date",
|
</>
|
||||||
value: game.release_date.toLocaleDateString("en-us", {
|
)}
|
||||||
year: "numeric",
|
{!savedTimeout && "Сохранено"}
|
||||||
}),
|
</span>
|
||||||
},
|
</span>
|
||||||
{ name: "Объём загрузки", key: "download_size" },
|
|
||||||
],
|
{game.description && (
|
||||||
].map((section, i) => (
|
<div
|
||||||
<ul key={i} className="w-[48%] bg-bg1 rounded-lg py-1 px-4">
|
contentEditable={editable}
|
||||||
{section.map((req) => (
|
suppressContentEditableWarning={true}
|
||||||
<li key={req.name} className="font-bold text-sm lp:text-md py-1">
|
className={clsx(
|
||||||
{req.name + ": "}
|
"text-md text-justify",
|
||||||
<input
|
"text-fg4 pt-2 outline-none",
|
||||||
readOnly={!editable}
|
!editable && "cursor-default"
|
||||||
className={clsx(
|
)}
|
||||||
"font-normal outline-none bg-bg1",
|
{...register("description", { value: game.description })}
|
||||||
req.value === undefined && "text-fg4"
|
onInput={(e) => {
|
||||||
)}
|
setValue("description", e.currentTarget.innerText, {
|
||||||
{...register(req.key as keyof GameCreateType)}
|
shouldValidate: true,
|
||||||
defaultValue={
|
shouldDirty: true,
|
||||||
req.value ??
|
});
|
||||||
(game[req.key as keyof GameType] as string) ??
|
}}
|
||||||
"Не известно"
|
>
|
||||||
}
|
{game.description}
|
||||||
></input>
|
</div>
|
||||||
</li>
|
)}
|
||||||
))}
|
</span>
|
||||||
</ul>
|
<div
|
||||||
))}
|
className={clsx(
|
||||||
</div>
|
"w-full flex justify-between pt-4",
|
||||||
{game.trailer && getYouTubeID(game.trailer) && (
|
!editable && "cursor-default"
|
||||||
<iframe
|
)}
|
||||||
src={"https://youtube.com/embed/" + getYouTubeID(game.trailer)}
|
>
|
||||||
className="w-full aspect-video rounded-lg mt-4"
|
{(
|
||||||
allowFullScreen
|
[
|
||||||
/>
|
[
|
||||||
)}
|
{ name: "Система", key: "system" },
|
||||||
<div className="relative w-full flex items-center justify-around pt-4">
|
{ name: "Процессор", key: "processor" },
|
||||||
<Link
|
{ name: "Оперативная память", key: "memory" },
|
||||||
href={process.env.NEXT_PUBLIC_CONTENT_URL + "/" + game.torrent_file}
|
{ name: "Видеокарта", key: "graphics" },
|
||||||
className="p-4 bg-ac0 text-fg1 text-2xl rounded-lg"
|
{ name: "Место на диске", key: "storage" },
|
||||||
>
|
],
|
||||||
Скачать {game.title}
|
[
|
||||||
</Link>
|
{ name: "Версия игры", key: "version" },
|
||||||
</div>
|
{
|
||||||
<div className="w-full flex justify-end">
|
name: "Дата обновления раздачи",
|
||||||
<Link
|
key: "update_date",
|
||||||
className="text-right text-sm relative top-4 lp:-top-4"
|
value: game.update_date.toLocaleDateString("ru-ru"),
|
||||||
href="/how_to_download"
|
},
|
||||||
>
|
{ name: "Язык", key: "language" },
|
||||||
Как скачать игру
|
{ name: "Разработчик", key: "developer" },
|
||||||
<br /> с помощью .torrent файла?
|
{
|
||||||
</Link>
|
name: "Год выхода",
|
||||||
</div>
|
key: "release_date",
|
||||||
<input type="submit" className=" m-2 p-2 bg-ac0" />
|
value: game.release_date.toLocaleDateString("en-us", {
|
||||||
</form>
|
year: "numeric",
|
||||||
);
|
}),
|
||||||
|
},
|
||||||
|
{ name: "Объём загрузки", key: "download_size" },
|
||||||
|
],
|
||||||
|
] as { name: string; key: keyof GameCreateType; value?: string }[][]
|
||||||
|
).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 &&
|
||||||
|
(game[req.key] === undefined ||
|
||||||
|
game[req.key] === propertyUnknownText) &&
|
||||||
|
"text-fg4",
|
||||||
|
!editable && "cursor-default"
|
||||||
|
)}
|
||||||
|
{...register(req.key, {
|
||||||
|
value:
|
||||||
|
req.value ??
|
||||||
|
(game[req.key] as string) ??
|
||||||
|
propertyUnknownText,
|
||||||
|
})}
|
||||||
|
defaultValue={
|
||||||
|
req.value ??
|
||||||
|
(game[req.key] as string) ??
|
||||||
|
propertyUnknownText
|
||||||
|
}
|
||||||
|
onBlur={(e) => {
|
||||||
|
if (e.target.value === "") {
|
||||||
|
e.target.value = propertyUnknownText;
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
></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="hidden" />
|
||||||
|
</form>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user