mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Add games page
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
NEXT_PUBLIC_BASE_URL=http://127.0.0.1:3000/api
|
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
|
NEXT_PUBLIC_COVER_FULL_URL=http://127.0.0.1:8000/content/images/cover/full_size
|
||||||
|
NEXT_PUBLIC_COVER_PREVIEW_URL=http://127.0.0.1:8000/content/images/cover/preview
|
||||||
25
src/app/games/page.tsx
Normal file
25
src/app/games/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import { GameService } from "@/entities/game";
|
||||||
|
import { GameCard } from "@/features/gameCard";
|
||||||
|
import { Section } from "@/widgets/section";
|
||||||
|
import { Metadata } from "next";
|
||||||
|
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: ".Torrent: Игры",
|
||||||
|
description:
|
||||||
|
".Torrent: Игры - каталог .torrent файлов для обмена видеоиграми",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function Home() {
|
||||||
|
const gameCards = await GameService.getGameCards();
|
||||||
|
return (
|
||||||
|
<div className="w-full h-full max-w-[var(--app-width)] m-auto overflow-y-auto">
|
||||||
|
{gameCards && (
|
||||||
|
<Section>
|
||||||
|
{gameCards.map((card) => (
|
||||||
|
<GameCard key={card.id} card={card} />
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -7,24 +7,25 @@ import { Header } from "@/widgets/header";
|
|||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: ".Torrent",
|
title: ".Torrent",
|
||||||
description: ".torrent file sharing service",
|
description:
|
||||||
|
".Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
// suppressHydrationWarning only for html for theme support
|
// suppressHydrationWarning for theme support
|
||||||
<html lang="ru" suppressHydrationWarning>
|
<html lang="ru" suppressHydrationWarning>
|
||||||
<body className={inter.className}>
|
<body className={inter.className}>
|
||||||
<ThemeProvider enableSystem={false} defaultTheme="light">
|
<ThemeProvider enableSystem={false} defaultTheme="light">
|
||||||
<Header />
|
<Header />
|
||||||
{children}
|
{children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import { Section } from "@/widgets/section";
|
|||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const gameCards = await GameService.getGameCards();
|
const gameCards = await GameService.getGameCards();
|
||||||
return (
|
return (
|
||||||
<div className="w-full max-w-[var(--app-width)] m-auto">
|
<div className="w-full h-full max-w-[var(--app-width)] m-auto overflow-y-auto">
|
||||||
{gameCards && (
|
{gameCards && (
|
||||||
<Section name="Игры">
|
<Section
|
||||||
|
name="Игры"
|
||||||
|
link="/games"
|
||||||
|
invite_text={'Перейти в раздел "Игры"'}
|
||||||
|
>
|
||||||
{gameCards.map((card) => (
|
{gameCards.map((card) => (
|
||||||
<GameCard key={card.id} card={card} />
|
<GameCard key={card.id} card={card} />
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -1,20 +1,27 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const gameCardSchema = z.object({
|
export const gameCardSchema = z
|
||||||
id: z.number(),
|
.object({
|
||||||
title: z.string().min(3),
|
id: z.number(),
|
||||||
cover: z
|
title: z.string().min(3),
|
||||||
.string()
|
cover: z.string().optional(),
|
||||||
.optional()
|
description: z.string().optional(),
|
||||||
.transform((u) => {
|
release_date: z
|
||||||
if (!!u) return process.env.NEXT_PUBLIC_COVER_FULL_URL + "/" + u;
|
.string()
|
||||||
}),
|
.min(1)
|
||||||
description: z.string().optional(),
|
.transform((d) => new Date(d)),
|
||||||
release_date: z
|
})
|
||||||
.string()
|
.transform((card) => {
|
||||||
.min(1)
|
return {
|
||||||
.transform((d) => new Date(d)),
|
...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 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 => {
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ import Image from "next/image";
|
|||||||
export const GameCard = ({ card }: { card: GameCardType }) => {
|
export const GameCard = ({ card }: { card: GameCardType }) => {
|
||||||
return (
|
return (
|
||||||
<div className="group/gamecard cursor-pointer">
|
<div className="group/gamecard cursor-pointer">
|
||||||
{!!card.cover && (
|
{!!card.cover_preview && (
|
||||||
<Image
|
<Image
|
||||||
src={card.cover}
|
src={card.cover_preview}
|
||||||
className="rounded-lg"
|
className="rounded-lg"
|
||||||
alt=""
|
alt=""
|
||||||
width={700}
|
width={700}
|
||||||
|
|||||||
@@ -1,17 +1,41 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export const Section = ({
|
export const Section = ({
|
||||||
name,
|
name,
|
||||||
|
invite_text,
|
||||||
|
link,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name?: string;
|
||||||
|
invite_text?: string;
|
||||||
|
link?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// open section onClick
|
<section className="w-full h-fit p-5 mb-20 pt-8">
|
||||||
<section className="w-full p-5 pt-8">
|
{name && (
|
||||||
<h2 className="text-4xl pb-2 cursor-pointer w-fit">{name}</h2>
|
<h2
|
||||||
|
className="text-4xl pb-2 cursor-pointer w-fit"
|
||||||
|
onClick={() => link && router.push(link)}
|
||||||
|
>
|
||||||
|
{name}
|
||||||
|
</h2>
|
||||||
|
)}
|
||||||
<div className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-y-10 gap-x-3">
|
<div className="grid grid-cols-1 tb:grid-cols-2 lp:grid-cols-3 gap-y-10 gap-x-3">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
|
{link && invite_text && (
|
||||||
|
<div className="w-full flex justify-end pt-5">
|
||||||
|
<Link href={link} className="text-lg">
|
||||||
|
{invite_text}
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user