mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 20:30: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} */
|
/** @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;
|
export default nextConfig;
|
||||||
|
|||||||
11
package-lock.json
generated
11
package-lock.json
generated
@@ -12,7 +12,8 @@
|
|||||||
"next": "14.2.3",
|
"next": "14.2.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18"
|
"react-dom": "^18",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
@@ -4821,6 +4822,14 @@
|
|||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": "14.2.3",
|
||||||
"next-themes": "^0.3.0",
|
"next-themes": "^0.3.0",
|
||||||
"react": "^18",
|
"react": "^18",
|
||||||
"react-dom": "^18"
|
"react-dom": "^18",
|
||||||
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
|||||||
@@ -1,6 +1,18 @@
|
|||||||
|
import { GameService } from "@/entities/game";
|
||||||
import { GameCard } from "@/features/gameCard";
|
import { GameCard } from "@/features/gameCard";
|
||||||
import { Section } from "@/widgets/section";
|
import { Section } from "@/widgets/section";
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home() {
|
||||||
return <div className="w-full max-w-[var(--app-width)] m-auto"></div>;
|
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 = () => {
|
import { GameCardType } from "@/entities/game";
|
||||||
return <></>;
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -49,7 +49,7 @@ export const Header = () => {
|
|||||||
<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-all h-0 flex items-center relative bottom-3"
|
transition-opacity h-0 flex items-center relative bottom-3"
|
||||||
>
|
>
|
||||||
<SearchIcon className="w-4 h-4 mx-2" />
|
<SearchIcon className="w-4 h-4 mx-2" />
|
||||||
Поиск
|
Поиск
|
||||||
|
|||||||
@@ -7,9 +7,11 @@ export const Section = ({
|
|||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
// open section onClick
|
// open section onClick
|
||||||
<section className="w-full m-5 mt-8 cursor-pointer">
|
<section className="w-full p-5 pt-8">
|
||||||
<h2 className="text-4xl">{name}</h2>
|
<h2 className="text-4xl pb-2 cursor-pointer w-fit">{name}</h2>
|
||||||
<div>{children}</div>
|
<div className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-y-10 gap-x-3">
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user