mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Game create and edit form. Formatting (2 spaces)
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LoginForm,
|
LoginForm,
|
||||||
loginFormFieldNames,
|
loginFormFieldNames,
|
||||||
loginFormSchema,
|
loginFormSchema,
|
||||||
} from "@/entities/user";
|
} from "@/entities/user";
|
||||||
import { UserService } from "@/entities/user/user";
|
import { UserService } from "@/entities/user/user";
|
||||||
import { Modal } from "@/shared/ui";
|
import { Modal } from "@/shared/ui";
|
||||||
@@ -13,60 +13,60 @@ import { SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import { mutate } from "swr";
|
import { mutate } from "swr";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
formState: { errors },
|
formState: { errors },
|
||||||
} = useForm<LoginForm>({ resolver: zodResolver(loginFormSchema) });
|
} = useForm<LoginForm>({ resolver: zodResolver(loginFormSchema) });
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const onSubmit: SubmitHandler<LoginForm> = async (data) => {
|
const onSubmit: SubmitHandler<LoginForm> = async (data) => {
|
||||||
const userInfo = await UserService.Login(data);
|
const userInfo = await UserService.Login(data);
|
||||||
mutate("user", userInfo);
|
mutate("user", userInfo);
|
||||||
router.back();
|
router.back();
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal>
|
<Modal>
|
||||||
<div className="">
|
<div className="">
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="flex flex-col items-center justify-evenly"
|
className="flex flex-col items-center justify-evenly"
|
||||||
>
|
>
|
||||||
<h2 className="pb-4 text-4xl">.Torrent</h2>
|
<h2 className="pb-4 text-4xl">.Torrent</h2>
|
||||||
{(["username", "password"] as (keyof LoginForm)[]).map((field) => (
|
{(["username", "password"] as (keyof LoginForm)[]).map((field) => (
|
||||||
<label
|
<label
|
||||||
className="flex flex-col items-start relative w-64 py-1"
|
className="flex flex-col items-start relative w-64 py-1"
|
||||||
key={field}
|
key={field}
|
||||||
>
|
>
|
||||||
<input
|
<input
|
||||||
{...register(field)}
|
{...register(field)}
|
||||||
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
className="peer/search w-full rounded-lg bg-bg4 px-2 h-10"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
<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-opacity h-0 flex items-center relative bottom-5 left-4
|
transition-opacity h-0 flex items-center relative bottom-5 left-4
|
||||||
text-lg"
|
text-lg"
|
||||||
>
|
>
|
||||||
{loginFormFieldNames[field]}
|
{loginFormFieldNames[field]}
|
||||||
</span>
|
</span>
|
||||||
<p className="text-sm text-err w-full text-center">
|
<p className="text-sm text-err w-full text-center">
|
||||||
{errors[field]?.message}
|
{errors[field]?.message}
|
||||||
</p>
|
</p>
|
||||||
</label>
|
</label>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Войти"
|
value="Войти"
|
||||||
className="bg-ac0 mt-2 p-1 px-4 rounded-lg"
|
className="bg-ac0 mt-2 p-1 px-4 rounded-lg"
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,27 +4,27 @@ import { GameInfo } from "@/widgets/gameInfo";
|
|||||||
import { Section } from "@/widgets/section";
|
import { Section } from "@/widgets/section";
|
||||||
|
|
||||||
export default async function Games({
|
export default async function Games({
|
||||||
params: { game_id },
|
params: { game_id },
|
||||||
}: {
|
}: {
|
||||||
params: { game_id: number };
|
params: { game_id: number };
|
||||||
}) {
|
}) {
|
||||||
const gameCards = await GameService.getGameCards();
|
const gameCards = await GameService.GetGameCards();
|
||||||
const game = await GameService.getGame(game_id);
|
const game = await GameService.GetGame(game_id);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{game && <GameInfo game={game} />}
|
{game && <GameInfo game={game} />}
|
||||||
|
|
||||||
{gameCards && (
|
{gameCards && (
|
||||||
<Section
|
<Section
|
||||||
name="Популярные игры"
|
name="Популярные игры"
|
||||||
link="/games"
|
link="/games"
|
||||||
invite_text={'Перейти в раздел "Игры"'}
|
invite_text={'Перейти в раздел "Игры"'}
|
||||||
>
|
>
|
||||||
{gameCards.map((card) => (
|
{gameCards.map((card) => (
|
||||||
<GameCard key={card.id} card={card} />
|
<GameCard key={card.id} card={card} />
|
||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
26
src/app/games/add/page.tsx
Normal file
26
src/app/games/add/page.tsx
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import { GameService } from "@/entities/game";
|
||||||
|
import { GameCard } from "@/features/gameCard";
|
||||||
|
import { GameInfo } from "@/widgets/gameInfo";
|
||||||
|
import { Section } from "@/widgets/section";
|
||||||
|
|
||||||
|
export default async function AddGame() {
|
||||||
|
const gameCards = await GameService.GetGameCards();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<GameInfo game={GameService.GetEmptyGame()} />
|
||||||
|
|
||||||
|
{gameCards && (
|
||||||
|
<Section
|
||||||
|
name="Популярные игры"
|
||||||
|
link="/games"
|
||||||
|
invite_text={'Перейти в раздел "Игры"'}
|
||||||
|
>
|
||||||
|
{gameCards.map((card) => (
|
||||||
|
<GameCard key={card.id} card={card} />
|
||||||
|
))}
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -10,7 +10,7 @@ export const metadata: Metadata = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export default async function Games() {
|
export default async function Games() {
|
||||||
const gameCards = await GameService.getGameCards();
|
const gameCards = await GameService.GetGameCards();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{gameCards && gameCards.length > 0 && (
|
{gameCards && gameCards.length > 0 && (
|
||||||
|
|||||||
@@ -1,61 +1,61 @@
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: ".Torrent: Как скачать?",
|
title: ".Torrent: Как скачать?",
|
||||||
description:
|
description:
|
||||||
".Torrent: Как скачать? - краткое руководство по скачиваю данных с помощью .torrent файлов",
|
".Torrent: Как скачать? - краткое руководство по скачиваю данных с помощью .torrent файлов",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function HowToDownload() {
|
export default async function HowToDownload() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full flex flex-col lp:flex-row justify-between p-4">
|
<div className="w-full flex flex-col lp:flex-row justify-between p-4">
|
||||||
<div className="w-full p-4 lp:w-[50%] lp:pr-10">
|
<div className="w-full p-4 lp:w-[50%] lp:pr-10">
|
||||||
<h1 className="text-4xl lp:text-3xl">Как скачать?</h1>
|
<h1 className="text-4xl lp:text-3xl">Как скачать?</h1>
|
||||||
<div className="text-fg4 text-justify pt-2">
|
<div className="text-fg4 text-justify pt-2">
|
||||||
Чтобы скачать данные с помощью торрент-файла, выполните следующие
|
Чтобы скачать данные с помощью торрент-файла, выполните следующие
|
||||||
шаги:
|
шаги:
|
||||||
<ul className="*:text-fg4">
|
<ul className="*:text-fg4">
|
||||||
<li>
|
<li>
|
||||||
1. Загрузите торрент-файл, содержащий информацию о файлах, которые
|
1. Загрузите торрент-файл, содержащий информацию о файлах, которые
|
||||||
вы хотите скачать с нашего сайта.
|
вы хотите скачать с нашего сайта.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
2. Откройте программу-клиент для загрузки торрентов, например,
|
2. Откройте программу-клиент для загрузки торрентов, например,
|
||||||
uTorrent, BitTorrent или qBittorrent.
|
uTorrent, BitTorrent или qBittorrent.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
3. В программе-клиенте выберите опцию "Open Torrent File" или "Add
|
3. В программе-клиенте выберите опцию "Open Torrent
|
||||||
Torrent" и выберите торрент-файл, который вы скачали в первом
|
File" или "Add Torrent" и выберите торрент-файл,
|
||||||
шаге.
|
который вы скачали в первом шаге.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
4. После этого начнется загрузка файлов, указанных в
|
4. После этого начнется загрузка файлов, указанных в
|
||||||
торрент-файле. Вы также можете выбрать папку, куда сохранять
|
торрент-файле. Вы также можете выбрать папку, куда сохранять
|
||||||
скачанные файлы.
|
скачанные файлы.
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
5. Дождитесь завершения загрузки файлов. После этого вы сможете
|
5. Дождитесь завершения загрузки файлов. После этого вы сможете
|
||||||
открыть и использовать скачанные файлы на своем компьютере.
|
открыть и использовать скачанные файлы на своем компьютере.
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full p-4 lp:w-[50%] lp:pl-10">
|
<div className="w-full p-4 lp:w-[50%] lp:pl-10">
|
||||||
<h2 className="text-4xl lp:text-3xl">Что такое .torrent файл?</h2>
|
<h2 className="text-4xl lp:text-3xl">Что такое .torrent файл?</h2>
|
||||||
<p className="text-fg4 text-justify pt-2">
|
<p className="text-fg4 text-justify pt-2">
|
||||||
Торрент-файл (или .torrent-файл) - это небольшой файл, который
|
Торрент-файл (или .torrent-файл) - это небольшой файл, который
|
||||||
содержит метаданные о файле или наборе файлов, которые можно загрузить
|
содержит метаданные о файле или наборе файлов, которые можно загрузить
|
||||||
с помощью протокола BitTorrent. В торрент-файле обычно указан адрес
|
с помощью протокола BitTorrent. В торрент-файле обычно указан адрес
|
||||||
трекера (специального сервера, отслеживающего пиров) и хеш-суммы
|
трекера (специального сервера, отслеживающего пиров) и хеш-суммы
|
||||||
частей файлов, которые необходимы для скачивания.
|
частей файлов, которые необходимы для скачивания.
|
||||||
<br />
|
<br />
|
||||||
<br /> Пользователь, желающий загрузить файл через BitTorrent, сначала
|
<br /> Пользователь, желающий загрузить файл через BitTorrent, сначала
|
||||||
скачивает торрент-файл или magnet-ссылку, загружает ее в
|
скачивает торрент-файл или magnet-ссылку, загружает ее в
|
||||||
торрент-клиент (программу для скачивания торрентов), и затем начинает
|
торрент-клиент (программу для скачивания торрентов), и затем начинает
|
||||||
загрузку файлов, участвуя в обмене данными с другими пользователями
|
загрузку файлов, участвуя в обмене данными с другими пользователями
|
||||||
(пирами) через сеть BitTorrent.
|
(пирами) через сеть BitTorrent.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,27 +6,27 @@ import { Header } from "@/widgets/header";
|
|||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
|
|
||||||
export default function RootLayout({
|
export default function RootLayout({
|
||||||
auth,
|
auth,
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
auth: React.ReactNode;
|
auth: React.ReactNode;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
// suppressHydrationWarning 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">
|
||||||
{auth}
|
{auth}
|
||||||
<Header />
|
<Header />
|
||||||
<div
|
<div
|
||||||
className="w-full h-[calc(100%_-_5rem)] \
|
className="w-full h-[calc(100%_-_5rem)] \
|
||||||
max-w-[var(--app-width)] m-auto overflow-y-auto"
|
max-w-[var(--app-width)] m-auto overflow-y-auto"
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
export default function Login() {
|
export default function Login() {
|
||||||
redirect("/");
|
redirect("/");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,26 +4,26 @@ import { Section } from "@/widgets/section";
|
|||||||
import { Metadata } from "next";
|
import { Metadata } from "next";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: ".Torrent",
|
title: ".Torrent",
|
||||||
description:
|
description:
|
||||||
".Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг",
|
".Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг",
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const gameCards = await GameService.getGameCards();
|
const gameCards = await GameService.GetGameCards();
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{gameCards && gameCards.length > 0 && (
|
{gameCards && gameCards.length > 0 && (
|
||||||
<Section
|
<Section
|
||||||
name="Игры"
|
name="Игры"
|
||||||
link="/games"
|
link="/games"
|
||||||
invite_text={'Перейти в раздел "Игры"'}
|
invite_text={'Перейти в раздел "Игры"'}
|
||||||
>
|
>
|
||||||
{gameCards.map((card) => (
|
{gameCards.map((card) => (
|
||||||
<GameCard key={card.id} card={card} />
|
<GameCard key={card.id} card={card} />
|
||||||
))}
|
))}
|
||||||
</Section>
|
</Section>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,4 +13,16 @@ export abstract class FilesService {
|
|||||||
false
|
false
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static async UploadTorrent(torrent: File) {
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("torrent", torrent);
|
||||||
|
return await HTTPService.post(
|
||||||
|
`/files/torrent`,
|
||||||
|
coverNameSchema,
|
||||||
|
formData,
|
||||||
|
{},
|
||||||
|
false
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
import { HTTPService } from "@/shared/utils/http";
|
import { HTTPService } from "@/shared/utils/http";
|
||||||
import { gameCardsSchema, GameCardType } from "./schemas/gameCard";
|
import { gameCardsSchema } from "./schemas/gameCard";
|
||||||
import { GameCreateType, gameSchema } from "./schemas/game";
|
import { GameCreateType, gameSchema } from "./schemas/game";
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
export abstract class GameService {
|
export abstract class GameService {
|
||||||
public static async getGameCards() {
|
public static async GetGameCards() {
|
||||||
return await HTTPService.get("/games/cards", gameCardsSchema);
|
return await HTTPService.get("/games/cards", gameCardsSchema);
|
||||||
}
|
}
|
||||||
public static async getGame(id: number) {
|
public static async GetGame(id: number) {
|
||||||
return await HTTPService.get(`/games/${id}`, gameSchema);
|
return await HTTPService.get(`/games/${id}`, gameSchema);
|
||||||
}
|
}
|
||||||
public static async changeGame(id: number, gameInfo: GameCreateType) {
|
public static async ChangeGame(id: number, gameInfo: GameCreateType) {
|
||||||
return await HTTPService.put(`/games/${id}`, gameSchema, gameInfo);
|
return await HTTPService.put(`/games/${id}`, gameSchema, gameInfo);
|
||||||
}
|
}
|
||||||
|
public static async AddGame(gameInfo: GameCreateType) {
|
||||||
|
return await HTTPService.post(`/games`, gameSchema, gameInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GetEmptyGame(): GameCreateType {
|
||||||
|
return {
|
||||||
|
title: "",
|
||||||
|
torrent_file: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { GameType, GameCreateType, gameCreateSchema } from "./schemas/game";
|
|||||||
import { GameService } from "./game";
|
import { GameService } from "./game";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
GameService,
|
GameService,
|
||||||
gameCreateSchema,
|
gameCreateSchema,
|
||||||
type GameType,
|
type GameType,
|
||||||
type GameCreateType,
|
type GameCreateType,
|
||||||
type GameCardType,
|
type GameCardType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,23 +3,30 @@ import { gameCardBaseSchema } from "./gameCard";
|
|||||||
|
|
||||||
export const gameBaseSchema = gameCardBaseSchema.merge(
|
export const gameBaseSchema = gameCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
torrent_file: z.string().min(1),
|
torrent_file: z.string().min(3, "У раздачи должен быть .torrent файл"),
|
||||||
trailer: z.string().optional(),
|
trailer: z.string().optional(),
|
||||||
|
|
||||||
system: z.string().optional(),
|
system: z.string().optional().nullable(),
|
||||||
processor: z.string().optional(),
|
processor: z.string().optional().nullable(),
|
||||||
memory: z.string().optional(),
|
memory: z.string().optional().nullable(),
|
||||||
graphics: z.string().optional(),
|
graphics: z.string().optional().nullable(),
|
||||||
storage: z.string().optional(),
|
storage: z.string().optional().nullable(),
|
||||||
|
|
||||||
developer: z.string().optional(),
|
developer: z.string().optional().nullable(),
|
||||||
language: z.string().optional(),
|
language: z.string().optional().nullable(),
|
||||||
download_size: z.string().optional(),
|
download_size: z.string().optional().nullable(),
|
||||||
|
|
||||||
release_date: z
|
release_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.optional()
|
||||||
.transform((d) => new Date(d)),
|
.nullable()
|
||||||
|
.transform((d) =>
|
||||||
|
d
|
||||||
|
? new Date(d).toLocaleDateString("en-us", {
|
||||||
|
year: "numeric",
|
||||||
|
})
|
||||||
|
: undefined
|
||||||
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
export const gameCardBaseSchema = z.object({
|
export const gameCardBaseSchema = z.object({
|
||||||
title: z.string().min(3),
|
title: z.string().min(3, "Слишком короткое название"),
|
||||||
cover: z.string().optional(),
|
cover: z.string().optional().nullable(),
|
||||||
description: z.string().optional(),
|
description: z.string().optional().nullable(),
|
||||||
version: z.string().optional(),
|
version: z.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const gameCardSchema = gameCardBaseSchema.merge(
|
export const gameCardSchema = gameCardBaseSchema.merge(
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import {
|
import {
|
||||||
loginFormSchema,
|
loginFormSchema,
|
||||||
loginFormFieldNames,
|
loginFormFieldNames,
|
||||||
LoginForm,
|
LoginForm,
|
||||||
} from "./schemas/auth";
|
} from "./schemas/auth";
|
||||||
import { userSchema, User } from "./schemas/user";
|
import { userSchema, User } from "./schemas/user";
|
||||||
import { UserService } from "./user";
|
import { UserService } from "./user";
|
||||||
|
|
||||||
export {
|
export {
|
||||||
loginFormSchema,
|
loginFormSchema,
|
||||||
loginFormFieldNames,
|
loginFormFieldNames,
|
||||||
UserService,
|
UserService,
|
||||||
userSchema,
|
userSchema,
|
||||||
type User,
|
type User,
|
||||||
type LoginForm,
|
type LoginForm,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,29 +2,29 @@ import { z } from "zod";
|
|||||||
import { userSchema } from "./user";
|
import { userSchema } from "./user";
|
||||||
|
|
||||||
export const loginFormSchema = z.object({
|
export const loginFormSchema = z.object({
|
||||||
username: z.string().min(3, "Логин слишком короткий"),
|
username: z.string().min(3, "Логин слишком короткий"),
|
||||||
password: z.string().min(3, "Пароль слишком короткий"),
|
password: z.string().min(3, "Пароль слишком короткий"),
|
||||||
});
|
});
|
||||||
export const loginFormFieldNames = {
|
export const loginFormFieldNames = {
|
||||||
username: "Логин",
|
username: "Логин",
|
||||||
password: "Пароль",
|
password: "Пароль",
|
||||||
};
|
};
|
||||||
export type LoginForm = z.infer<typeof loginFormSchema>;
|
export type LoginForm = z.infer<typeof loginFormSchema>;
|
||||||
|
|
||||||
export const tokenResponseSchema = z
|
export const tokenResponseSchema = z
|
||||||
.object({
|
.object({
|
||||||
access_token: z.string(),
|
access_token: z.string(),
|
||||||
token_type: z.string(),
|
token_type: z.string(),
|
||||||
})
|
})
|
||||||
.transform((tokenResponse) => tokenResponse.access_token);
|
.transform((tokenResponse) => tokenResponse.access_token);
|
||||||
export type TokenResponse = z.infer<typeof tokenResponseSchema>;
|
export type TokenResponse = z.infer<typeof tokenResponseSchema>;
|
||||||
|
|
||||||
export const tokenDataSchema = userSchema.merge(
|
export const tokenDataSchema = userSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
expire: z
|
expire: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
export type TokenData = z.infer<typeof tokenDataSchema>;
|
export type TokenData = z.infer<typeof tokenDataSchema>;
|
||||||
|
|||||||
@@ -4,14 +4,14 @@ import { SunIcon } from "@/shared/assets/icons";
|
|||||||
import { useTheme } from "next-themes";
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
export const ColorSchemeSwitch = () => {
|
export const ColorSchemeSwitch = () => {
|
||||||
const { theme, setTheme } = useTheme();
|
const { theme, setTheme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SunIcon
|
<SunIcon
|
||||||
className="mr-5 h-8 w-8 cursor-pointer"
|
className="mr-5 h-8 w-8 cursor-pointer"
|
||||||
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
|
onClick={() => setTheme(theme == "light" ? "dark" : "light")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,29 +5,29 @@ import { useState } from "react";
|
|||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
|
||||||
export const Modal = ({ children }: { children: React.ReactNode }) => {
|
export const Modal = ({ children }: { children: React.ReactNode }) => {
|
||||||
const [closing, setClosing] = useState(false);
|
const [closing, setClosing] = useState(false);
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
!closing && "animate-fadeIn",
|
!closing && "animate-fadeIn",
|
||||||
closing && "animate-fadeOut opacity-0",
|
closing && "animate-fadeOut opacity-0",
|
||||||
"flex items-center justify-around",
|
"flex items-center justify-around",
|
||||||
"absolute z-20 left-0 w-full h-full bg-[#000000c5]"
|
"absolute z-20 left-0 w-full h-full bg-[#000000c5]"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setClosing(true);
|
setClosing(true);
|
||||||
setTimeout(() => router.back(), 500);
|
setTimeout(() => router.back(), 500);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="rounded-lg bg-bg1 w-fit h-fit p-6"
|
className="rounded-lg bg-bg1 w-fit h-fit p-6"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
export const getYouTubeID = (url: string) => {
|
export const getYouTubeID = (url: string) => {
|
||||||
const regExp =
|
const regExp =
|
||||||
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
/^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||||
const match = url.match(regExp);
|
const match = url.match(regExp);
|
||||||
return match && match[7].length == 11 ? match[7] : false;
|
return match && match[7].length == 11 ? match[7] : false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,7 +31,11 @@ export abstract class HTTPService {
|
|||||||
else throw Error("Response ok = false");
|
else throw Error("Response ok = false");
|
||||||
})
|
})
|
||||||
.then((r) => r.json())
|
.then((r) => r.json())
|
||||||
.then((d) => schema.parse(d) as z.infer<Z>)
|
.then((d) => {
|
||||||
|
const parsed = schema.safeParse(d);
|
||||||
|
if (parsed.success) return parsed.data as z.infer<Z>;
|
||||||
|
else throw new Error(parsed.error.message);
|
||||||
|
})
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import Link from "next/link";
|
|||||||
import { getYouTubeID } from "@/shared/utils";
|
import { getYouTubeID } from "@/shared/utils";
|
||||||
import { UserService } from "@/entities/user";
|
import { UserService } from "@/entities/user";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState, useMemo } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Img } from "@/shared/ui";
|
import { Img } from "@/shared/ui";
|
||||||
@@ -19,28 +19,44 @@ import { useDropzone } from "react-dropzone";
|
|||||||
import { FilesService } from "@/entities/files";
|
import { FilesService } from "@/entities/files";
|
||||||
import { SpinnerIcon } from "@/shared/assets/icons";
|
import { SpinnerIcon } from "@/shared/assets/icons";
|
||||||
|
|
||||||
const propertyUnknownText = "Не известно";
|
const isExistingGame = (game: GameCreateType | GameType): game is GameType => {
|
||||||
|
return (game as GameType).id !== undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const GameInfo = ({
|
||||||
|
game: init_game,
|
||||||
|
}: {
|
||||||
|
game: GameCreateType | GameType;
|
||||||
|
}) => {
|
||||||
|
const [game, changeGame] = useState<GameCreateType | GameType>(init_game);
|
||||||
|
|
||||||
export const GameInfo = ({ game }: { game: GameType }) => {
|
|
||||||
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
const { data: me } = useSWR("user", () => UserService.IdentifyYourself());
|
||||||
const [editable, setEditable] = useState<boolean>(false);
|
const [editable, setEditable] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => setEditable(me?.id === game.owner_id), [me, game]);
|
useEffect(() => {
|
||||||
|
if (me) {
|
||||||
|
if (isExistingGame(game)) setEditable(me.id === game.owner_id);
|
||||||
|
else setEditable(true);
|
||||||
|
}
|
||||||
|
}, [me, game]);
|
||||||
|
|
||||||
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const {
|
const {
|
||||||
register,
|
register,
|
||||||
handleSubmit,
|
handleSubmit,
|
||||||
setValue,
|
setValue,
|
||||||
watch,
|
watch,
|
||||||
|
reset,
|
||||||
formState: { dirtyFields, errors },
|
formState: { dirtyFields, errors },
|
||||||
} = useForm<GameCreateType>({
|
} = useForm<GameCreateType>({
|
||||||
|
defaultValues: game,
|
||||||
resolver: zodResolver(gameCreateSchema),
|
resolver: zodResolver(gameCreateSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
register("torrent_file", { value: game.torrent_file });
|
register("torrent_file", { value: game.torrent_file });
|
||||||
register("cover", { value: game.cover });
|
register("cover", { value: game.cover });
|
||||||
}, []);
|
}, [game.cover, game.torrent_file, register]);
|
||||||
|
|
||||||
const [savedTimeout, changeSavedTimeout] = useState<NodeJS.Timeout | null>(
|
const [savedTimeout, changeSavedTimeout] = useState<NodeJS.Timeout | null>(
|
||||||
null
|
null
|
||||||
@@ -48,6 +64,7 @@ export const GameInfo = ({ game }: { game: GameType }) => {
|
|||||||
const watchedData = watch();
|
const watchedData = watch();
|
||||||
const [formData, changeFormData] = useState<GameCreateType | null>(null);
|
const [formData, changeFormData] = useState<GameCreateType | null>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log(watchedData);
|
||||||
if (!Object.keys(dirtyFields).length) return;
|
if (!Object.keys(dirtyFields).length) return;
|
||||||
if (JSON.stringify(watchedData) === JSON.stringify(formData)) return;
|
if (JSON.stringify(watchedData) === JSON.stringify(formData)) return;
|
||||||
console.log(dirtyFields);
|
console.log(dirtyFields);
|
||||||
@@ -55,76 +72,141 @@ export const GameInfo = ({ game }: { game: GameType }) => {
|
|||||||
if (savedTimeout) clearTimeout(savedTimeout);
|
if (savedTimeout) clearTimeout(savedTimeout);
|
||||||
changeSavedTimeout(
|
changeSavedTimeout(
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
console.log("call", formRef.current);
|
||||||
if (formRef.current) formRef.current.requestSubmit();
|
if (formRef.current) formRef.current.requestSubmit();
|
||||||
}, 5000)
|
}, 3000)
|
||||||
);
|
);
|
||||||
}, [watchedData]);
|
}, [watchedData]);
|
||||||
|
|
||||||
const onSubmit = async (formData: GameCreateType) => {
|
const onSubmit = async (formData: GameCreateType) => {
|
||||||
changeSavedTimeout(null);
|
changeSavedTimeout(null);
|
||||||
const updatedGame = await GameService.changeGame(game.id, formData);
|
if (isExistingGame(game)) {
|
||||||
console.log(updatedGame);
|
const updatedGame = await GameService.ChangeGame(game.id, formData);
|
||||||
|
if (updatedGame) {
|
||||||
|
changeGame(updatedGame);
|
||||||
|
reset({}, { keepValues: true });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const addedGame = await GameService.AddGame(formData);
|
||||||
|
if (addedGame) {
|
||||||
|
changeGame(addedGame);
|
||||||
|
reset({}, { keepValues: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const [cover, setCover] = useState<string | undefined>(game.cover);
|
const onCoverDrop = useCallback(
|
||||||
|
(acceptedFiles: File[]) => {
|
||||||
|
const file = acceptedFiles[0];
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = async () => {
|
||||||
|
const coverName = await FilesService.UploadCover(file);
|
||||||
|
if (coverName) {
|
||||||
|
setValue("cover", coverName, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileReader.readAsDataURL(file);
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
const {
|
||||||
const file = acceptedFiles[0];
|
getRootProps: getCoverDropRootProps,
|
||||||
const fileReader = new FileReader();
|
getInputProps: getCoverDropInputProps,
|
||||||
fileReader.onload = async () => {
|
isDragActive: isCoverDragActive,
|
||||||
const coverName = await FilesService.UploadCover(file);
|
} = useDropzone({ onDrop: onCoverDrop });
|
||||||
if (coverName) {
|
|
||||||
setCover(coverName);
|
|
||||||
setValue("cover", coverName);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
fileReader.readAsDataURL(file);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });
|
const onTorrentDrop = useCallback(
|
||||||
|
(acceptedFiles: File[]) => {
|
||||||
|
const file = acceptedFiles[0];
|
||||||
|
const fileReader = new FileReader();
|
||||||
|
fileReader.onload = async () => {
|
||||||
|
const torrentName = await FilesService.UploadTorrent(file);
|
||||||
|
if (torrentName) {
|
||||||
|
setValue("torrent_file", torrentName, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fileReader.readAsDataURL(file);
|
||||||
|
},
|
||||||
|
[setValue]
|
||||||
|
);
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
useEffect(() => console.log(errors), [errors]);
|
||||||
|
|
||||||
|
const {
|
||||||
|
getRootProps: getTorrentDropRootProps,
|
||||||
|
getInputProps: getTorrentDropInputProps,
|
||||||
|
isDragActive: isTorrentDragActive,
|
||||||
|
} = useDropzone({ onDrop: onTorrentDrop });
|
||||||
|
|
||||||
|
const [trailer, setTrailer] = useState<string | undefined>(game.trailer);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={handleSubmit(onSubmit)}
|
||||||
className="p-4 flex flex-col lp:block"
|
className="m-4 flex flex-col lp:block"
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
>
|
>
|
||||||
{cover && (
|
{(watchedData.cover || editable) && (
|
||||||
<div
|
<div className="lp:w-[60%] lp:px-4 lp:pl-0 pt-2 pb-4 float-left relative">
|
||||||
className="lp:w-[60%] lp:px-4 lp:pl-0 pt-2 float-left"
|
{watchedData.cover && (
|
||||||
{...(editable ? getRootProps() : {})}
|
<Img
|
||||||
>
|
src={watchedData.cover}
|
||||||
<Img
|
preview={false}
|
||||||
src={cover}
|
className="transition-all rounded-lg w-full object-contain"
|
||||||
preview={false}
|
width={1280}
|
||||||
className="transition-all rounded-lg w-full object-contain"
|
height={720}
|
||||||
width={1280}
|
/>
|
||||||
height={720}
|
)}
|
||||||
/>
|
{!watchedData.cover && editable && (
|
||||||
|
<div className="w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
||||||
|
)}
|
||||||
|
|
||||||
{editable && (
|
{editable && (
|
||||||
<>
|
<div
|
||||||
<input {...getInputProps()} />
|
className="absolute h-full w-full mt-1 left-0 top-0 z-20 flex items-end"
|
||||||
<span className="flex items-center ju w-full p-1">
|
{...(editable ? getCoverDropRootProps() : {})}
|
||||||
{isDragActive ? (
|
>
|
||||||
<p>Изменить обложку...</p>
|
<input {...getCoverDropInputProps()} />
|
||||||
|
<span className="flex items-center ju w-full">
|
||||||
|
{isCoverDragActive ? (
|
||||||
|
<p className="w-full text-sm pl-2">Изменить обложку...</p>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-sm w-full flex justify-around">
|
<>
|
||||||
Для редактирования нажмите или перетащите новую обложку
|
<span className="hidden lp:flex text-sm w-full justify-around text-fg4">
|
||||||
поверх старой
|
Для редактирования нажмите или перетащите новую обложку
|
||||||
</span>
|
поверх старой
|
||||||
|
</span>
|
||||||
|
<span className="text-xs lp:hidden w-full flex justify-around text-fg4">
|
||||||
|
Для редактирования нажмите на обложку и выберите фото
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
</>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<span className="lp:max-w-[40%]">
|
<span>
|
||||||
<span className="flex items-end justify-between">
|
<span className="flex items-end justify-between relative pt-2">
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"text-fg4 text-2xl absolute -z-10 opacity-0",
|
||||||
|
watchedData.title === "" && "opacity-100",
|
||||||
|
"transition-opacity cursor-text"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Введите название
|
||||||
|
</span>
|
||||||
<h1
|
<h1
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"text-4xl outline-none max-w-[80%]",
|
"text-4xl outline-none max-w-[80%] cursor-text",
|
||||||
!editable && "cursor-default"
|
!editable && "cursor-default"
|
||||||
)}
|
)}
|
||||||
suppressContentEditableWarning={true}
|
suppressContentEditableWarning={true}
|
||||||
@@ -135,41 +217,61 @@ export const GameInfo = ({ game }: { game: GameType }) => {
|
|||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
});
|
});
|
||||||
console.log();
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{game.title}
|
{game.title}
|
||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm text-fg4 flex items-center">
|
|
||||||
{savedTimeout && (
|
|
||||||
<>
|
|
||||||
<SpinnerIcon className="mr-2" />
|
|
||||||
Редактируется
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!savedTimeout && "Сохранено"}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
|
|
||||||
{game.description && (
|
{editable && (
|
||||||
<div
|
<span className="text-sm text-fg4 flex items-center cursor-default">
|
||||||
contentEditable={editable}
|
{savedTimeout && Object.keys(errors).length === 0 && (
|
||||||
suppressContentEditableWarning={true}
|
<>
|
||||||
className={clsx(
|
<SpinnerIcon className="mr-2" />
|
||||||
"text-md text-justify",
|
Редактируется
|
||||||
"text-fg4 pt-2 outline-none",
|
</>
|
||||||
!editable && "cursor-default"
|
)}
|
||||||
)}
|
{savedTimeout && Object.keys(errors).length > 0 && (
|
||||||
{...register("description", { value: game.description })}
|
<span className="text-err text-right">Некорректные данные</span>
|
||||||
onInput={(e) => {
|
)}
|
||||||
setValue("description", e.currentTarget.innerText, {
|
{!savedTimeout && "Сохранено"}
|
||||||
shouldValidate: true,
|
</span>
|
||||||
shouldDirty: true,
|
)}
|
||||||
});
|
</span>
|
||||||
}}
|
<div className="text-err text-xs w-full h-2">
|
||||||
>
|
{errors.title?.message}
|
||||||
{game.description}
|
</div>
|
||||||
</div>
|
{(game.description || editable) && (
|
||||||
|
<span className="relative">
|
||||||
|
<span
|
||||||
|
className={clsx(
|
||||||
|
"text-fg4 text-md absolute -z-10 opacity-0",
|
||||||
|
(watchedData.description === "" ||
|
||||||
|
watchedData.description === undefined) &&
|
||||||
|
"opacity-100",
|
||||||
|
"transition-opacity mt-2"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Введите описание
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
contentEditable={editable}
|
||||||
|
suppressContentEditableWarning={true}
|
||||||
|
className={clsx(
|
||||||
|
"text-md text-justify",
|
||||||
|
"text-fg4 pt-2 outline-none",
|
||||||
|
!editable && "cursor-default"
|
||||||
|
)}
|
||||||
|
{...register("description", { value: game.description })}
|
||||||
|
onInput={(e) => {
|
||||||
|
setValue("description", e.currentTarget.innerText, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{game.description}
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div
|
<div
|
||||||
@@ -192,71 +294,142 @@ export const GameInfo = ({ game }: { game: GameType }) => {
|
|||||||
{
|
{
|
||||||
name: "Дата обновления раздачи",
|
name: "Дата обновления раздачи",
|
||||||
key: "update_date",
|
key: "update_date",
|
||||||
value: game.update_date.toLocaleDateString("ru-ru"),
|
value: isExistingGame(game)
|
||||||
|
? game.update_date.toLocaleDateString("ru-ru")
|
||||||
|
: new Date().toLocaleDateString("ru-ru"),
|
||||||
|
edit: false,
|
||||||
},
|
},
|
||||||
{ name: "Язык", key: "language" },
|
{ name: "Язык", key: "language" },
|
||||||
{ name: "Разработчик", key: "developer" },
|
{ name: "Разработчик", key: "developer" },
|
||||||
{
|
{
|
||||||
name: "Год выхода",
|
name: "Год выхода",
|
||||||
key: "release_date",
|
key: "release_date",
|
||||||
value: game.release_date.toLocaleDateString("en-us", {
|
|
||||||
year: "numeric",
|
|
||||||
}),
|
|
||||||
},
|
},
|
||||||
{ name: "Объём загрузки", key: "download_size" },
|
{ name: "Объём загрузки", key: "download_size" },
|
||||||
],
|
],
|
||||||
] as { name: string; key: keyof GameCreateType; value?: string }[][]
|
] as {
|
||||||
|
name: string;
|
||||||
|
key: keyof GameCreateType;
|
||||||
|
value?: string;
|
||||||
|
edit?: boolean;
|
||||||
|
}[][]
|
||||||
).map((section, i) => (
|
).map((section, i) => (
|
||||||
<ul key={i} className="w-[48%] bg-bg1 rounded-lg py-1 px-4">
|
<ul key={i} className="w-[48%] bg-bg1 rounded-lg py-1 px-4">
|
||||||
{section.map((req) => (
|
{section.map((req) => (
|
||||||
<li key={req.name} className="font-bold text-sm lp:text-md py-1">
|
<li key={req.name} className="text-sm lp:text-md py-1">
|
||||||
{req.name + ": "}
|
<span className="font-bold">{req.name + ": "}</span>
|
||||||
<input
|
<span className="relative">
|
||||||
readOnly={!editable}
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"font-normal outline-none bg-bg1",
|
"text-fg4 opacity-0 absolute -z-10 text-xs",
|
||||||
req.value === undefined &&
|
(watchedData[req.key] === undefined ||
|
||||||
(game[req.key] === undefined ||
|
watchedData[req.key] === null ||
|
||||||
game[req.key] === propertyUnknownText) &&
|
(watchedData[req.key] as string) === "") &&
|
||||||
"text-fg4",
|
"opacity-100 relative !z-10 text-base",
|
||||||
!editable && "cursor-default"
|
"transition-opacity "
|
||||||
)}
|
)}
|
||||||
{...register(req.key, {
|
>
|
||||||
value:
|
Не известно
|
||||||
req.value ??
|
</span>
|
||||||
(game[req.key] as string) ??
|
<span
|
||||||
propertyUnknownText,
|
className={clsx(
|
||||||
})}
|
"outline-none",
|
||||||
defaultValue={
|
!editable && "cursor-default",
|
||||||
req.value ??
|
(watchedData[req.key] === undefined ||
|
||||||
(game[req.key] as string) ??
|
watchedData[req.key] === null ||
|
||||||
propertyUnknownText
|
(watchedData[req.key] as string) === "") &&
|
||||||
}
|
"opacity-100 absolute left-0 top-0 inline-block min-w-10 z-10"
|
||||||
onBlur={(e) => {
|
)}
|
||||||
if (e.target.value === "") {
|
{...register(req.key, {
|
||||||
e.target.value = propertyUnknownText;
|
value: req.value ?? (game[req.key] as string),
|
||||||
}
|
})}
|
||||||
}}
|
contentEditable={editable && (req.edit ?? true)}
|
||||||
></input>
|
suppressContentEditableWarning={true}
|
||||||
|
onInput={(e) => {
|
||||||
|
setValue(req.key, e.currentTarget.innerText, {
|
||||||
|
shouldValidate: true,
|
||||||
|
shouldDirty: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{req.value ?? (game[req.key] as string)}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
{game.trailer && getYouTubeID(game.trailer) && (
|
{(trailer || editable) && (
|
||||||
<iframe
|
<div className="w-ful aspect-video">
|
||||||
src={"https://youtube.com/embed/" + getYouTubeID(game.trailer)}
|
{trailer && getYouTubeID(trailer) && (
|
||||||
className="w-full aspect-video rounded-lg mt-4"
|
<iframe
|
||||||
allowFullScreen
|
src={"https://youtube.com/embed/" + getYouTubeID(trailer)}
|
||||||
/>
|
className="w-full aspect-video rounded-lg mt-4"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{!trailer && editable && (
|
||||||
|
<div className="mt-4 w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="relative w-full flex items-center justify-around pt-4">
|
|
||||||
<Link
|
{editable && (
|
||||||
href={process.env.NEXT_PUBLIC_CONTENT_URL + "/" + game.torrent_file}
|
<div className="w-full flex justify-end pt-1">
|
||||||
className="p-4 bg-ac0 text-fg1 text-2xl rounded-lg"
|
<input
|
||||||
>
|
className="outline-none w-full lp:w-2/3 text-xs lp:text-base bg-bg1"
|
||||||
Скачать {game.title}
|
{...register("trailer", {
|
||||||
</Link>
|
value: game.trailer,
|
||||||
|
onChange: (e) => {
|
||||||
|
setTrailer(e.target.value);
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
defaultValue={game.trailer}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className="relative w-full flex items-center justify-around pt-4"
|
||||||
|
{...(editable ? getTorrentDropRootProps() : {})}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
process.env.NEXT_PUBLIC_CONTENT_URL +
|
||||||
|
"/" +
|
||||||
|
watchedData.torrent_file
|
||||||
|
}
|
||||||
|
className={clsx(
|
||||||
|
"p-4 bg-ac0 text-fg1 text-2xl rounded-lg",
|
||||||
|
!watchedData.torrent_file && "bg-bg1 text-fg4"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
Скачать {watchedData.title}
|
||||||
|
</Link>
|
||||||
|
{editable && (
|
||||||
|
<>
|
||||||
|
<input {...getTorrentDropInputProps()} />
|
||||||
|
<span className="flex flex-col items-center w-full p-1 text-fg4 text-xs lp:text-sm cursor-pointer">
|
||||||
|
{errors.torrent_file && (
|
||||||
|
<span className="w-full text-center text-err">
|
||||||
|
{errors.torrent_file.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isTorrentDragActive ? (
|
||||||
|
<span className="w-full text-center">
|
||||||
|
Изменить .torrent файл...
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<span className="w-full text-center">
|
||||||
|
Нажмите, чтобы изменить <br />
|
||||||
|
.torrent файл
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full flex justify-end">
|
<div className="w-full flex justify-end">
|
||||||
<Link
|
<Link
|
||||||
|
|||||||
@@ -9,62 +9,62 @@ import clsx from "clsx";
|
|||||||
import { UserActivities } from "@/features/userActivities";
|
import { UserActivities } from "@/features/userActivities";
|
||||||
|
|
||||||
const sections = [
|
const sections = [
|
||||||
{ title: "Игры", href: "games" },
|
{ title: "Игры", href: "games" },
|
||||||
{ title: "Фильмы", href: "films" },
|
{ title: "Фильмы", href: "films" },
|
||||||
{ title: "Аудиокниги", href: "audiobooks" },
|
{ title: "Аудиокниги", href: "audiobooks" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const currentPageName = useSelectedLayoutSegment();
|
const currentPageName = useSelectedLayoutSegment();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header className="w-full h-20 z-10 bg-bg1 sticky top-0 shadow-xl">
|
<header className="w-full h-20 z-10 bg-bg1 sticky top-0 shadow-xl">
|
||||||
<div
|
<div
|
||||||
className="w-full h-full max-w-[var(--app-width)] m-auto px-5
|
className="w-full h-full max-w-[var(--app-width)] m-auto px-5
|
||||||
flex items-center justify-between"
|
flex items-center justify-between"
|
||||||
>
|
>
|
||||||
<h1 className="text-4xl font-bold flex items-center">
|
<h1 className="text-4xl font-bold flex items-center">
|
||||||
<div className="lp:hidden">
|
<div className="lp:hidden">
|
||||||
<MobileMenu sections={sections} />
|
<MobileMenu sections={sections} />
|
||||||
</div>
|
</div>
|
||||||
<Link href="/">.Torrent</Link>
|
<Link href="/">.Torrent</Link>
|
||||||
</h1>
|
</h1>
|
||||||
<div className="hidden text-2xl lp:block">
|
<div className="hidden text-2xl lp:block">
|
||||||
{sections.map((section) => (
|
{sections.map((section) => (
|
||||||
<Link
|
<Link
|
||||||
key={section.title}
|
key={section.title}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"px-5 cursor-pointer hover:underline underline-offset-2",
|
"px-5 cursor-pointer hover:underline underline-offset-2",
|
||||||
currentPageName === section.href && "underline"
|
currentPageName === section.href && "underline"
|
||||||
)}
|
)}
|
||||||
href={section.href}
|
href={section.href}
|
||||||
>
|
>
|
||||||
{section.title}
|
{section.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col items-end">
|
<div className="flex flex-col items-end">
|
||||||
<span className="flex items-center mb-1 ">
|
<span className="flex items-center mb-1 ">
|
||||||
<SchemeSwitch />
|
<SchemeSwitch />
|
||||||
<UserActivities />
|
<UserActivities />
|
||||||
</span>
|
</span>
|
||||||
<label className="flex flex-col items-start relative w-36">
|
<label className="flex flex-col items-start relative w-36">
|
||||||
<input
|
<input
|
||||||
type="search"
|
type="search"
|
||||||
className="peer/search w-full rounded-lg bg-bg4 px-2"
|
className="peer/search w-full rounded-lg bg-bg4 px-2"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
/>
|
/>
|
||||||
<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-opacity 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" />
|
||||||
Поиск
|
Поиск
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,46 +5,46 @@ import Link from "next/link";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
export const MobileMenu = ({
|
export const MobileMenu = ({
|
||||||
sections,
|
sections,
|
||||||
}: {
|
}: {
|
||||||
sections: { title: string; href: string }[];
|
sections: { title: string; href: string }[];
|
||||||
}) => {
|
}) => {
|
||||||
const [open, changeMenuOpen] = useState<boolean>(false);
|
const [open, changeMenuOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<button
|
<button
|
||||||
className="w-16 h-16 *:w-12 *:h-1 *:bg-fg1 *:my-3
|
className="w-16 h-16 *:w-12 *:h-1 *:bg-fg1 *:my-3
|
||||||
*:transition-all *:duration-300 *:relative"
|
*:transition-all *:duration-300 *:relative"
|
||||||
onClick={() => changeMenuOpen(!open)}
|
onClick={() => changeMenuOpen(!open)}
|
||||||
onBlur={() => changeMenuOpen(false)}
|
onBlur={() => changeMenuOpen(false)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(open && "rotate-45 top-4", !open && "top-0")}
|
className={clsx(open && "rotate-45 top-4", !open && "top-0")}
|
||||||
></div>
|
></div>
|
||||||
<div className={clsx(open && "opacity-0")}></div>
|
<div className={clsx(open && "opacity-0")}></div>
|
||||||
<div
|
<div
|
||||||
className={clsx(open && "-rotate-45 bottom-4", !open && "bottom-0")}
|
className={clsx(open && "-rotate-45 bottom-4", !open && "bottom-0")}
|
||||||
></div>
|
></div>
|
||||||
</button>
|
</button>
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"h-0 absolute transition-all duration-300 overflow-hidden\
|
"h-0 absolute transition-all duration-300 overflow-hidden\
|
||||||
bg-bg4 rounded-lg px-4 flex flex-col shadow-xl",
|
bg-bg4 rounded-lg px-4 flex flex-col shadow-xl",
|
||||||
open && "h-32"
|
open && "h-32"
|
||||||
)}
|
)}
|
||||||
onClick={() => changeMenuOpen(false)}
|
onClick={() => changeMenuOpen(false)}
|
||||||
>
|
>
|
||||||
{sections.map((section) => (
|
{sections.map((section) => (
|
||||||
<Link
|
<Link
|
||||||
key={section.title}
|
key={section.title}
|
||||||
className="text-xl py-2 cursor-pointer hover:underline"
|
className="text-xl py-2 cursor-pointer hover:underline"
|
||||||
href={section.href}
|
href={section.href}
|
||||||
>
|
>
|
||||||
{section.title}
|
{section.title}
|
||||||
</Link>
|
</Link>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -8,50 +8,50 @@ import Masonry, { ResponsiveMasonry } from "react-responsive-masonry";
|
|||||||
import { boolean } from "zod";
|
import { boolean } from "zod";
|
||||||
|
|
||||||
export const Section = ({
|
export const Section = ({
|
||||||
name,
|
name,
|
||||||
invite_text,
|
invite_text,
|
||||||
link,
|
link,
|
||||||
children,
|
children,
|
||||||
}: {
|
}: {
|
||||||
name?: string;
|
name?: string;
|
||||||
invite_text?: string;
|
invite_text?: string;
|
||||||
link?: string;
|
link?: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [loaded, setLoaded] = useState<boolean>(false);
|
const [loaded, setLoaded] = useState<boolean>(false);
|
||||||
|
|
||||||
useEffect(() => setLoaded(true), []);
|
useEffect(() => setLoaded(true), []);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="w-full h-fit p-2 mb-20 pt-8">
|
<section className="w-full h-fit p-2 mb-20 pt-8">
|
||||||
{name && (
|
{name && (
|
||||||
<h2
|
<h2
|
||||||
className="text-4xl pb-2 cursor-pointer w-fit"
|
className="text-4xl pb-2 cursor-pointer w-fit"
|
||||||
onClick={() => link && router.push(link)}
|
onClick={() => link && router.push(link)}
|
||||||
>
|
>
|
||||||
{name}
|
{name}
|
||||||
</h2>
|
</h2>
|
||||||
)}
|
)}
|
||||||
<ResponsiveMasonry
|
<ResponsiveMasonry
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"transition-opacity duration-300 opacity-0",
|
"transition-opacity duration-300 opacity-0",
|
||||||
loaded && "opacity-100"
|
loaded && "opacity-100"
|
||||||
)}
|
)}
|
||||||
columnsCountBreakPoints={{ 0: 1, 640: 2, 1024: 3 }}
|
columnsCountBreakPoints={{ 0: 1, 640: 2, 1024: 3 }}
|
||||||
>
|
>
|
||||||
<Masonry gutter="1rem">{children}</Masonry>
|
<Masonry gutter="1rem">{children}</Masonry>
|
||||||
</ResponsiveMasonry>
|
</ResponsiveMasonry>
|
||||||
{link && invite_text && (
|
{link && invite_text && (
|
||||||
<div className="w-full flex justify-end pt-5">
|
<div className="w-full flex justify-end pt-5">
|
||||||
<Link
|
<Link
|
||||||
href={link}
|
href={link}
|
||||||
className="text-lg hover:underline underline-offset-4"
|
className="text-lg hover:underline underline-offset-4"
|
||||||
>
|
>
|
||||||
{invite_text}
|
{invite_text}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,52 +1,52 @@
|
|||||||
import type { Config } from "tailwindcss";
|
import type { Config } from "tailwindcss";
|
||||||
|
|
||||||
const config: Config = {
|
const config: Config = {
|
||||||
content: [
|
content: [
|
||||||
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/components/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/app/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
"./src/**/*.{js,ts,jsx,tsx,mdx}",
|
||||||
],
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
backgroundImage: {
|
backgroundImage: {
|
||||||
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
"gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
|
||||||
"gradient-conic":
|
"gradient-conic":
|
||||||
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
"conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
|
||||||
},
|
},
|
||||||
colors: {
|
colors: {
|
||||||
bg0: "var(--color-bg0)",
|
bg0: "var(--color-bg0)",
|
||||||
bg1: "var(--color-bg1)",
|
bg1: "var(--color-bg1)",
|
||||||
bg4: "var(--color-bg4)",
|
bg4: "var(--color-bg4)",
|
||||||
fg0: "var(--color-fg0)",
|
fg0: "var(--color-fg0)",
|
||||||
fg1: "var(--color-fg1)",
|
fg1: "var(--color-fg1)",
|
||||||
fg4: "var(--color-fg4)",
|
fg4: "var(--color-fg4)",
|
||||||
ac0: "var(--color-ac0)",
|
ac0: "var(--color-ac0)",
|
||||||
ac1: "var(--color-ac1)",
|
ac1: "var(--color-ac1)",
|
||||||
ac2: "var(--color-ac2)",
|
ac2: "var(--color-ac2)",
|
||||||
err: "var(--color-err)",
|
err: "var(--color-err)",
|
||||||
},
|
},
|
||||||
animation: {
|
animation: {
|
||||||
fadeIn: "fadeIn 0.25s ease-in-out",
|
fadeIn: "fadeIn 0.25s ease-in-out",
|
||||||
fadeOut: "fadeOut 0.25s ease-in-out",
|
fadeOut: "fadeOut 0.25s ease-in-out",
|
||||||
},
|
},
|
||||||
keyframes: () => ({
|
keyframes: () => ({
|
||||||
fadeIn: {
|
fadeIn: {
|
||||||
"0%": { opacity: "0" },
|
"0%": { opacity: "0" },
|
||||||
"100%": { opacity: "1" },
|
"100%": { opacity: "1" },
|
||||||
},
|
},
|
||||||
fadeOut: {
|
fadeOut: {
|
||||||
"0%": { opacity: "1" },
|
"0%": { opacity: "1" },
|
||||||
"100%": { opacity: "0" },
|
"100%": { opacity: "0" },
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
screens: {
|
screens: {
|
||||||
tb: "640px",
|
tb: "640px",
|
||||||
lp: "1024px",
|
lp: "1024px",
|
||||||
dsk: "1280px",
|
dsk: "1280px",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: [],
|
plugins: [],
|
||||||
};
|
};
|
||||||
export default config;
|
export default config;
|
||||||
|
|||||||
Reference in New Issue
Block a user