mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Add game cards and dev reverse proxy
This commit is contained in:
2
.env.development
Normal file
2
.env.development
Normal file
@@ -0,0 +1,2 @@
|
||||
NEXT_PUBLIC_BASE_URL=http://127.0.0.1:3000/api
|
||||
NEXT_PUBLIC_COVER_FULL_URL=http://127.0.0.1:8000/content/images/cover/full_size
|
||||
@@ -1,4 +1,24 @@
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {};
|
||||
const nextConfig = {
|
||||
async rewrites() {
|
||||
return process.env.NODE_ENV == "development"
|
||||
? [
|
||||
{
|
||||
source: "/api/:path*",
|
||||
destination: "http://127.0.0.1:8000/:path*",
|
||||
},
|
||||
]
|
||||
: [];
|
||||
},
|
||||
images: {
|
||||
remotePatterns: [
|
||||
{
|
||||
protocol: "http",
|
||||
hostname: "127.0.0.1",
|
||||
port: "8000",
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -12,7 +12,8 @@
|
||||
"next": "14.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
"react-dom": "^18",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
@@ -4821,6 +4822,14 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.23.8",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz",
|
||||
"integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@
|
||||
"next": "14.2.3",
|
||||
"next-themes": "^0.3.0",
|
||||
"react": "^18",
|
||||
"react-dom": "^18"
|
||||
"react-dom": "^18",
|
||||
"zod": "^3.23.8"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
||||
@@ -1,6 +1,18 @@
|
||||
import { GameService } from "@/entities/game";
|
||||
import { GameCard } from "@/features/gameCard";
|
||||
import { Section } from "@/widgets/section";
|
||||
|
||||
export default function Home() {
|
||||
return <div className="w-full max-w-[var(--app-width)] m-auto"></div>;
|
||||
export default async function Home() {
|
||||
const gameCards = await GameService.getGameCards();
|
||||
return (
|
||||
<div className="w-full max-w-[var(--app-width)] m-auto">
|
||||
{gameCards && (
|
||||
<Section name="Игры">
|
||||
{gameCards.map((card) => (
|
||||
<GameCard key={card.id} card={card} />
|
||||
))}
|
||||
</Section>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
11
src/entities/game/game.ts
Normal file
11
src/entities/game/game.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { HTTPService } from "@/shared/http/httpService";
|
||||
import { gameCardsSchema, GameCardType } from "./schemas/gameCard";
|
||||
|
||||
export abstract class GameService {
|
||||
public static async getGameCards() {
|
||||
return await HTTPService.get<GameCardType[]>(
|
||||
"/games/cards",
|
||||
gameCardsSchema
|
||||
);
|
||||
}
|
||||
}
|
||||
5
src/entities/game/index.ts
Normal file
5
src/entities/game/index.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { GameCardType } from "./schemas/gameCard";
|
||||
import { GameType } from "./schemas/game";
|
||||
import { GameService } from "./game";
|
||||
|
||||
export { GameService, type GameType, type GameCardType };
|
||||
35
src/entities/game/schemas/game.ts
Normal file
35
src/entities/game/schemas/game.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { z } from "zod";
|
||||
import { gameCardSchema } from "./gameCard";
|
||||
|
||||
export const gameSchema = z.union([
|
||||
gameCardSchema,
|
||||
z.object({
|
||||
torrent_file: z.string().min(1),
|
||||
language: z.string().optional(),
|
||||
version: z.string().optional(),
|
||||
download_size: z.string().optional(),
|
||||
system: z.string().optional(),
|
||||
processor: z.string().optional(),
|
||||
memory: z.string().optional(),
|
||||
graphics: z.string().optional(),
|
||||
storage: z.string().optional(),
|
||||
upload_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
}),
|
||||
]);
|
||||
export type GameType = z.infer<typeof gameSchema>;
|
||||
|
||||
export const isGame = (a: any): a is GameType => {
|
||||
return gameSchema.safeParse(a).success;
|
||||
};
|
||||
|
||||
export const gamesSchema = z.array(z.any()).transform((a) => {
|
||||
const games: GameType[] = [];
|
||||
a.forEach((e) => {
|
||||
if (isGame(e)) games.push(gameSchema.parse(e));
|
||||
else console.error("Game parse error - ", e);
|
||||
});
|
||||
return games;
|
||||
});
|
||||
31
src/entities/game/schemas/gameCard.ts
Normal file
31
src/entities/game/schemas/gameCard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const gameCardSchema = z.object({
|
||||
id: z.number(),
|
||||
title: z.string().min(3),
|
||||
cover: z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((u) => {
|
||||
if (!!u) return process.env.NEXT_PUBLIC_COVER_FULL_URL + "/" + u;
|
||||
}),
|
||||
description: z.string().optional(),
|
||||
release_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
});
|
||||
export type GameCardType = z.infer<typeof gameCardSchema>;
|
||||
|
||||
export const isGameCard = (a: any): a is GameCardType => {
|
||||
return gameCardSchema.safeParse(a).success;
|
||||
};
|
||||
|
||||
export const gameCardsSchema = z.array(z.any()).transform((a) => {
|
||||
const cards: GameCardType[] = [];
|
||||
a.forEach((e) => {
|
||||
if (isGameCard(e)) cards.push(gameCardSchema.parse(e));
|
||||
else console.error("GameCard parse error - ", e);
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
@@ -1,3 +1,31 @@
|
||||
export const GameCard = () => {
|
||||
return <></>;
|
||||
import { GameCardType } from "@/entities/game";
|
||||
import Image from "next/image";
|
||||
|
||||
export const GameCard = ({ card }: { card: GameCardType }) => {
|
||||
return (
|
||||
<div className="group/gamecard cursor-pointer">
|
||||
{!!card.cover && (
|
||||
<Image
|
||||
src={card.cover}
|
||||
className="rounded-lg"
|
||||
alt=""
|
||||
width={700}
|
||||
height={400}
|
||||
/>
|
||||
)}
|
||||
<div className="flex items-center justify-between pr-2">
|
||||
<h2 className="text-2xl py-1 group-hover/gamecard:underline underline-offset-4">
|
||||
{card.title}
|
||||
</h2>
|
||||
<span className="text-sm text-fg4">
|
||||
{card.release_date.toLocaleDateString("ru-ru", {
|
||||
year: "numeric",
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-lg tb:text-sm pr-2 text-justify line-clamp-5 text-fg4">
|
||||
{card.description}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
34
src/shared/http/httpService.ts
Normal file
34
src/shared/http/httpService.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export abstract class HTTPService {
|
||||
public static async get<Z>(
|
||||
url: string,
|
||||
schema: z.ZodTypeAny
|
||||
): Promise<Z | null> {
|
||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
},
|
||||
cache: "no-cache",
|
||||
})
|
||||
.then((r) => {
|
||||
if (r && r.ok) return r;
|
||||
else throw Error("Response ok = false");
|
||||
})
|
||||
.then((r) => r.json())
|
||||
.then((d) => {
|
||||
const parseResult = schema.safeParse(d);
|
||||
if (parseResult.success) {
|
||||
return parseResult.data as Z;
|
||||
} else {
|
||||
console.error(parseResult.error);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return null;
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -48,8 +48,8 @@ export const Header = () => {
|
||||
/>
|
||||
<span
|
||||
className="peer-focus/search:opacity-0
|
||||
peer-[:not(:placeholder-shown)]/search:opacity-0
|
||||
transition-all h-0 flex items-center relative bottom-3"
|
||||
peer-[:not(:placeholder-shown)]/search:opacity-0
|
||||
transition-opacity h-0 flex items-center relative bottom-3"
|
||||
>
|
||||
<SearchIcon className="w-4 h-4 mx-2" />
|
||||
Поиск
|
||||
|
||||
@@ -7,9 +7,11 @@ export const Section = ({
|
||||
}) => {
|
||||
return (
|
||||
// open section onClick
|
||||
<section className="w-full m-5 mt-8 cursor-pointer">
|
||||
<h2 className="text-4xl">{name}</h2>
|
||||
<div>{children}</div>
|
||||
<section className="w-full p-5 pt-8">
|
||||
<h2 className="text-4xl pb-2 cursor-pointer w-fit">{name}</h2>
|
||||
<div className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-y-10 gap-x-3">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user