New readme, screenshots and small fixes
80
README.md
@@ -1,36 +1,54 @@
|
|||||||
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
# .Torrent frontend
|
||||||
|
> .Torrent - сервис обмена .torrent файлами видеоигр, фильмов и аудиокниг
|
||||||
|
|
||||||
## Getting Started
|
|
||||||
|
|
||||||
First, run the development server:
|
|
||||||
|
|
||||||
```bash
|
## Стек
|
||||||
|
- TypeScript
|
||||||
|
- React 18
|
||||||
|
- Next.js 14 (App Router)
|
||||||
|
- Tailwind CSS
|
||||||
|
- Zod
|
||||||
|
- SWR
|
||||||
|
- clsx
|
||||||
|
- React Hook Form
|
||||||
|
- и другие
|
||||||
|
- next-themes
|
||||||
|
- js-cookie
|
||||||
|
- jwt-decode
|
||||||
|
- react-dropzone
|
||||||
|
|
||||||
|
|
||||||
|
## Возможности
|
||||||
|
- Главная страница со списком популярных видеоигр, фильмов, аудиокниг
|
||||||
|
- Страницы со списками по категориям (отдельно видеоигры, фильмы, аудиокниги)
|
||||||
|
- Страница просмотра, редактирования или добавления сущности
|
||||||
|
- Форма входа или регистрации в виде модального окна с помощью Parallel и Intercepting маршрутов в Next.js
|
||||||
|
- Адаптивная верстка. Корректное отображение на мобильных устройствах, планшетах, ноутбуках, десктопах
|
||||||
|
- SEO оптимизация. SSR, метаданные к страницам
|
||||||
|
- Валидация данных с помощью Zod. Некорректные (или неполные) данные вырезаются (если некорректна одна сущность из списка, то остальные отображаются)
|
||||||
|
- Структура проекта в соответствии с Feature-Sliced Design
|
||||||
|
- Цветовая схема Gruvbox. Возможность переключения тёмной и светлой темы
|
||||||
|
- Вся конфигурация через файл .env (или переменные среды), для удобного запуска в Docker контейнере
|
||||||
|
|
||||||
|
## Скриншоты
|
||||||
|
|||
|
||||||
|
|-|-|
|
||||||
|
|||
|
||||||
|
|||
|
||||||
|
|||
|
||||||
|
|||
|
||||||
|
|
||||||
|
|
||||||
|
## Запуск
|
||||||
|
### Локально
|
||||||
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
# or
|
|
||||||
yarn dev
|
|
||||||
# or
|
|
||||||
pnpm dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
## ToDo
|
||||||
|
- [ ] Dockerfile
|
||||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
- [ ] Добавить ссылку на общий репозиторий
|
||||||
|
- [ ] Теги жанров для сущностей
|
||||||
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
|
- [ ] Поиск
|
||||||
|
- [ ] Динамические метаданные к страницам [section]/*
|
||||||
## Learn More
|
- [ ] Страница "О проекте"
|
||||||
|
|
||||||
To learn more about Next.js, take a look at the following resources:
|
|
||||||
|
|
||||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
|
||||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
|
||||||
|
|
||||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
|
|
||||||
|
|
||||||
## Deploy on Vercel
|
|
||||||
|
|
||||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
|
||||||
|
|
||||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
|
|
||||||
BIN
screenshots/game.png
Normal file
|
After Width: | Height: | Size: 656 KiB |
BIN
screenshots/game_create.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
screenshots/game_editing.png
Normal file
|
After Width: | Height: | Size: 356 KiB |
BIN
screenshots/login.png
Normal file
|
After Width: | Height: | Size: 363 KiB |
BIN
screenshots/main.png
Normal file
|
After Width: | Height: | Size: 613 KiB |
BIN
screenshots/main_mobile.png
Normal file
|
After Width: | Height: | Size: 439 KiB |
BIN
screenshots/movie.png
Normal file
|
After Width: | Height: | Size: 716 KiB |
BIN
screenshots/movie_mobile.png
Normal file
|
After Width: | Height: | Size: 547 KiB |
BIN
screenshots/movies.png
Normal file
|
After Width: | Height: | Size: 549 KiB |
BIN
screenshots/registration.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
5
src/app/registration/page.tsx
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
export default function Registration() {
|
||||||
|
redirect("/");
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@ import {
|
|||||||
TypesOfItems,
|
TypesOfItems,
|
||||||
UnionItemType,
|
UnionItemType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
import { EraseCacheByTag } from "@/shared/utils/http";
|
import { EraseCacheByTags } from "@/shared/utils/http";
|
||||||
|
|
||||||
export abstract class ItemService {
|
export abstract class ItemService {
|
||||||
private static get itemsConfiguration(): {
|
private static get itemsConfiguration(): {
|
||||||
@@ -122,13 +122,7 @@ export abstract class ItemService {
|
|||||||
const item = await this.itemsConfiguration[itemInfo.type].service.Add(
|
const item = await this.itemsConfiguration[itemInfo.type].service.Add(
|
||||||
itemInfo
|
itemInfo
|
||||||
);
|
);
|
||||||
|
this.UpdateCachedData(item);
|
||||||
if (item)
|
|
||||||
EraseCacheByTag(
|
|
||||||
`/${this.itemsConfiguration[itemInfo.type].service.urlPrefix}/${
|
|
||||||
item.id
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
public static async ChangeItem(id: number, itemInfo: ItemCreateType) {
|
public static async ChangeItem(id: number, itemInfo: ItemCreateType) {
|
||||||
@@ -136,12 +130,14 @@ export abstract class ItemService {
|
|||||||
id,
|
id,
|
||||||
itemInfo
|
itemInfo
|
||||||
);
|
);
|
||||||
if (item)
|
this.UpdateCachedData(item);
|
||||||
EraseCacheByTag(
|
|
||||||
`/${this.itemsConfiguration[itemInfo.type].service.urlPrefix}/${
|
|
||||||
item.id
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
return item;
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UpdateCachedData(item: ItemType | null) {
|
||||||
|
if (item) {
|
||||||
|
const tagPrefix = this.itemsConfiguration[item.type].service.urlPrefix;
|
||||||
|
EraseCacheByTags([`/${tagPrefix}/${item.id}`, `/${tagPrefix}/cards`]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,24 @@ type RequestOptions = GetRequestOptions & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export abstract class HTTPService {
|
export abstract class HTTPService {
|
||||||
|
private static deepUndefinedToNull(o?: object): object | undefined {
|
||||||
|
if (o)
|
||||||
|
return Object.fromEntries(
|
||||||
|
Object.entries(o).map(([k, v]) => {
|
||||||
|
if (v === undefined) return [k, null];
|
||||||
|
if (typeof v === "object") return [k, this.deepUndefinedToNull(v)];
|
||||||
|
return [k, v];
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
public static async request<Z extends z.ZodTypeAny>(
|
public static async request<Z extends z.ZodTypeAny>(
|
||||||
method: "GET" | "POST" | "PUT" | "DELETE",
|
method: "GET" | "POST" | "PUT" | "DELETE",
|
||||||
url: string,
|
url: string,
|
||||||
schema: Z,
|
schema: Z,
|
||||||
options?: RequestOptions
|
options?: RequestOptions
|
||||||
) {
|
) {
|
||||||
|
console.log(options?.body);
|
||||||
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
return await fetch(process.env.NEXT_PUBLIC_BASE_URL + url, {
|
||||||
method: method,
|
method: method,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -35,7 +47,9 @@ export abstract class HTTPService {
|
|||||||
body:
|
body:
|
||||||
(options?.stringify ?? true) != true
|
(options?.stringify ?? true) != true
|
||||||
? (options?.body as BodyInit)
|
? (options?.body as BodyInit)
|
||||||
: JSON.stringify(options?.body),
|
: JSON.stringify(
|
||||||
|
this.deepUndefinedToNull(options?.body as object | undefined)
|
||||||
|
),
|
||||||
cache: options?.cache ?? options?.next ? undefined : "no-cache",
|
cache: options?.cache ?? options?.next ? undefined : "no-cache",
|
||||||
next: options?.next ?? {},
|
next: options?.next ?? {},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ export const ItemFragment = ({
|
|||||||
className="relative w-full flex items-center justify-around pt-4"
|
className="relative w-full flex items-center justify-around pt-4"
|
||||||
{...(editable ? getFragmentDropRootProps() : {})}
|
{...(editable ? getFragmentDropRootProps() : {})}
|
||||||
>
|
>
|
||||||
<div className="flex flex-col items-center w-[80%] h-20">
|
<div className="flex flex-col items-center w-[80%] h-14 lp:h-20">
|
||||||
<audio
|
<audio
|
||||||
controls
|
controls
|
||||||
controlsList="nodownload"
|
controlsList="nodownload"
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ export const ItemInfo = <T extends ItemType | ItemCreateType>({
|
|||||||
|
|
||||||
const onSubmit = async (formData: ItemCreateType) => {
|
const onSubmit = async (formData: ItemCreateType) => {
|
||||||
changeSavedTimeout(null);
|
changeSavedTimeout(null);
|
||||||
|
console.log(formData);
|
||||||
const updatedItem = ItemService.isExistingItem(item)
|
const updatedItem = ItemService.isExistingItem(item)
|
||||||
? await ItemService.ChangeItem(item.id, formData)
|
? await ItemService.ChangeItem(item.id, formData)
|
||||||
: await ItemService.AddItem(formData);
|
: await ItemService.AddItem(formData);
|
||||||
|
|||||||