diff --git a/.env.development b/.env.development new file mode 100644 index 0000000..e558ab2 --- /dev/null +++ b/.env.development @@ -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 \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index 4678774..ea8e890 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -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; diff --git a/package-lock.json b/package-lock.json index 0e39b7e..3b671f5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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" + } } } } diff --git a/package.json b/package.json index a8f70ff..9f972d6 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/app/page.tsx b/src/app/page.tsx index 3ee6344..83f413a 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -1,6 +1,18 @@ +import { GameService } from "@/entities/game"; import { GameCard } from "@/features/gameCard"; import { Section } from "@/widgets/section"; -export default function Home() { - return
; +export default async function Home() { + const gameCards = await GameService.getGameCards(); + return ( +
+ {gameCards && ( +
+ {gameCards.map((card) => ( + + ))} +
+ )} +
+ ); } diff --git a/src/entities/game/game.ts b/src/entities/game/game.ts new file mode 100644 index 0000000..c16032a --- /dev/null +++ b/src/entities/game/game.ts @@ -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( + "/games/cards", + gameCardsSchema + ); + } +} diff --git a/src/entities/game/index.ts b/src/entities/game/index.ts new file mode 100644 index 0000000..6fa905e --- /dev/null +++ b/src/entities/game/index.ts @@ -0,0 +1,5 @@ +import { GameCardType } from "./schemas/gameCard"; +import { GameType } from "./schemas/game"; +import { GameService } from "./game"; + +export { GameService, type GameType, type GameCardType }; diff --git a/src/entities/game/schemas/game.ts b/src/entities/game/schemas/game.ts new file mode 100644 index 0000000..5131223 --- /dev/null +++ b/src/entities/game/schemas/game.ts @@ -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; + +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; +}); diff --git a/src/entities/game/schemas/gameCard.ts b/src/entities/game/schemas/gameCard.ts new file mode 100644 index 0000000..ac7b50f --- /dev/null +++ b/src/entities/game/schemas/gameCard.ts @@ -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; + +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; +}); diff --git a/src/features/gameCard/gameCard.tsx b/src/features/gameCard/gameCard.tsx index cf61e5f..024316d 100644 --- a/src/features/gameCard/gameCard.tsx +++ b/src/features/gameCard/gameCard.tsx @@ -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 ( +
+ {!!card.cover && ( + + )} +
+

+ {card.title} +

+ + {card.release_date.toLocaleDateString("ru-ru", { + year: "numeric", + })} + +
+

+ {card.description} +

+
+ ); }; diff --git a/src/shared/http/httpService.ts b/src/shared/http/httpService.ts new file mode 100644 index 0000000..e0b8888 --- /dev/null +++ b/src/shared/http/httpService.ts @@ -0,0 +1,34 @@ +import { z } from "zod"; + +export abstract class HTTPService { + public static async get( + url: string, + schema: z.ZodTypeAny + ): Promise { + 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; + }); + } +} diff --git a/src/widgets/header/header.tsx b/src/widgets/header/header.tsx index ead011c..e307e6c 100644 --- a/src/widgets/header/header.tsx +++ b/src/widgets/header/header.tsx @@ -48,8 +48,8 @@ export const Header = () => { /> Поиск diff --git a/src/widgets/section/section.tsx b/src/widgets/section/section.tsx index 90d07b8..3cc4da7 100644 --- a/src/widgets/section/section.tsx +++ b/src/widgets/section/section.tsx @@ -7,9 +7,11 @@ export const Section = ({ }) => { return ( // open section onClick -
-

{name}

-
{children}
+
+

{name}

+
+ {children} +
); };