Add game cards and dev reverse proxy

This commit is contained in:
2024-05-11 13:50:52 +04:00
parent 4f8301d1e8
commit a2ea5fc409
13 changed files with 202 additions and 12 deletions

2
.env.development Normal file
View 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

View File

@@ -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
View File

@@ -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"
}
}
}
}

View File

@@ -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",

View File

@@ -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
View 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
);
}
}

View 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 };

View 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;
});

View 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;
});

View File

@@ -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>
);
};

View 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;
});
}
}

View File

@@ -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" />
Поиск

View File

@@ -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>
);
};