mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 12:20:48 +04:00
Add genres and actors
This commit is contained in:
@@ -9,9 +9,9 @@
|
||||
- Next.js 14 (App Router)
|
||||
- Tailwind CSS
|
||||
- Zod
|
||||
- React Hook Form
|
||||
- SWR
|
||||
- clsx
|
||||
- React Hook Form
|
||||
- и другие
|
||||
- next-themes
|
||||
- js-cookie
|
||||
|
||||
@@ -46,7 +46,7 @@ export default function Login() {
|
||||
>
|
||||
<input
|
||||
{...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 outline-none"
|
||||
placeholder=" "
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
@@ -50,7 +50,7 @@ export default function Registration() {
|
||||
>
|
||||
<input
|
||||
{...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 outline-none"
|
||||
placeholder=" "
|
||||
autoComplete="off"
|
||||
/>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { isSection, ItemService, MovieService } from "@/entities/item";
|
||||
import { ItemService } from "@/entities/item";
|
||||
import { ItemCard } from "@/widgets/itemCard";
|
||||
import { Section } from "@/widgets/section";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
@@ -14,6 +14,12 @@ import {
|
||||
TypesOfItems,
|
||||
} from "../types";
|
||||
import { ItemService } from "../item";
|
||||
import {
|
||||
AudiobookGenreCreateType,
|
||||
audiobookGenreSchema,
|
||||
audiobookGenresSchema,
|
||||
} from "./schemas/genre";
|
||||
import { RequiredFrom } from "@/shared/utils/types";
|
||||
|
||||
@staticImplements<IItemService>()
|
||||
export abstract class AudiobookService {
|
||||
@@ -30,7 +36,7 @@ export abstract class AudiobookService {
|
||||
|
||||
public static async GetCards() {
|
||||
return await HTTPService.get(
|
||||
`/${this.urlPrefix}/cards`,
|
||||
`/${this.urlPrefix}`,
|
||||
audiobookCardsSchema,
|
||||
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||
);
|
||||
@@ -53,7 +59,7 @@ export abstract class AudiobookService {
|
||||
});
|
||||
}
|
||||
|
||||
public static GetEmpty(): AudiobookCreateType {
|
||||
public static GetEmpty(): RequiredFrom<AudiobookCreateType> {
|
||||
return {
|
||||
title: "",
|
||||
torrent_file: "",
|
||||
@@ -61,6 +67,23 @@ export abstract class AudiobookService {
|
||||
};
|
||||
}
|
||||
|
||||
public static async GetGenres() {
|
||||
return await HTTPService.get(
|
||||
`/genres/${this.urlPrefix}`,
|
||||
audiobookGenresSchema
|
||||
);
|
||||
}
|
||||
|
||||
public static async CreateGenre(info: AudiobookGenreCreateType) {
|
||||
return await HTTPService.post(
|
||||
`/genres/${this.urlPrefix}`,
|
||||
audiobookGenreSchema,
|
||||
{
|
||||
body: info,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static propertiesDescription: ItemPropertiesDescriptionType<AudiobookType> = [
|
||||
[
|
||||
{ name: "Автор", key: "author" },
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { audiobookCardBaseSchema } from "./audiobookCard";
|
||||
import { audiobookGenresSchema } from "./genre";
|
||||
import { ownerSchema } from "../../schemas/owner";
|
||||
import { TypesOfItems } from "../../types";
|
||||
|
||||
export const audiobookBaseSchema = audiobookCardBaseSchema.merge(
|
||||
z.object({
|
||||
@@ -22,6 +25,8 @@ export const audiobookBaseSchema = audiobookCardBaseSchema.merge(
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
|
||||
genres: audiobookGenresSchema.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -31,15 +36,11 @@ export type AudiobookCreateType = z.infer<typeof audiobookCreateSchema>;
|
||||
export const audiobookSchema = audiobookBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
owner_id: z.number().positive(),
|
||||
owner: ownerSchema,
|
||||
update_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
upload_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
})
|
||||
);
|
||||
export type AudiobookType = z.infer<typeof audiobookSchema>;
|
||||
@@ -56,3 +57,7 @@ export const audiobooksSchema = z.array(z.any()).transform((a) => {
|
||||
});
|
||||
return audiobooks;
|
||||
});
|
||||
|
||||
export const isAudiobook = (a: any): a is AudiobookType => {
|
||||
return (a as AudiobookType).type === TypesOfItems.audiobook;
|
||||
};
|
||||
|
||||
@@ -34,7 +34,3 @@ export const audiobookCardsSchema = z.array(z.any()).transform((a) => {
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
|
||||
export const isAudiobook = (a: any): a is AudiobookCardType => {
|
||||
return (a as AudiobookCardType).type === TypesOfItems.audiobook;
|
||||
};
|
||||
|
||||
32
src/entities/item/audiobook/schemas/genre.ts
Normal file
32
src/entities/item/audiobook/schemas/genre.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const audiobookGenreBaseSchema = z.object({
|
||||
genre: z.string().min(3),
|
||||
});
|
||||
|
||||
export const audiobookGenreCreateSchema = audiobookGenreBaseSchema.merge(
|
||||
z.object({})
|
||||
);
|
||||
export type AudiobookGenreCreateType = z.infer<
|
||||
typeof audiobookGenreCreateSchema
|
||||
>;
|
||||
|
||||
export const audiobookGenreSchema = audiobookGenreBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
})
|
||||
);
|
||||
export type AudiobookGenreType = z.infer<typeof audiobookGenreSchema>;
|
||||
|
||||
export const isAudiobookGenreStrict = (a: any): a is AudiobookGenreType => {
|
||||
return audiobookGenreSchema.safeParse(a).success;
|
||||
};
|
||||
|
||||
export const audiobookGenresSchema = z.array(z.any()).transform((a) => {
|
||||
const cards: AudiobookGenreType[] = [];
|
||||
a.forEach((e) => {
|
||||
if (isAudiobookGenreStrict(e)) cards.push(audiobookGenreSchema.parse(e));
|
||||
else console.error("AudiobookGenre parse error - ", e);
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
@@ -10,6 +10,12 @@ import {
|
||||
TypesOfItems,
|
||||
} from "../types";
|
||||
import { ItemService } from "../item";
|
||||
import {
|
||||
GameGenreCreateType,
|
||||
gameGenreSchema,
|
||||
gameGenresSchema,
|
||||
} from "./schemas/genre";
|
||||
import { RequiredFrom } from "@/shared/utils/types";
|
||||
|
||||
@staticImplements<IItemService>()
|
||||
export abstract class GameService {
|
||||
@@ -26,7 +32,7 @@ export abstract class GameService {
|
||||
|
||||
public static async GetCards() {
|
||||
return await HTTPService.get(
|
||||
`/${this.urlPrefix}/cards`,
|
||||
`/${this.urlPrefix}`,
|
||||
gameCardsSchema,
|
||||
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||
);
|
||||
@@ -49,7 +55,7 @@ export abstract class GameService {
|
||||
});
|
||||
}
|
||||
|
||||
public static GetEmpty(): GameCreateType {
|
||||
public static GetEmpty(): RequiredFrom<GameCreateType> {
|
||||
return {
|
||||
title: "",
|
||||
torrent_file: "",
|
||||
@@ -57,6 +63,20 @@ export abstract class GameService {
|
||||
};
|
||||
}
|
||||
|
||||
public static async GetGenres() {
|
||||
return await HTTPService.get(`/genres/${this.urlPrefix}`, gameGenresSchema);
|
||||
}
|
||||
|
||||
public static async CreateGenre(info: GameGenreCreateType) {
|
||||
return await HTTPService.post(
|
||||
`/genres/${this.urlPrefix}`,
|
||||
gameGenreSchema,
|
||||
{
|
||||
body: info,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static propertiesDescription: ItemPropertiesDescriptionType<GameType> = [
|
||||
[
|
||||
{ name: "Система", key: "system" },
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { z } from "zod";
|
||||
import { gameCardBaseSchema } from "./gameCard";
|
||||
import { gameGenresSchema } from "./genre";
|
||||
import { ownerSchema } from "../../schemas/owner";
|
||||
import { TypesOfItems } from "../../types";
|
||||
|
||||
export const gameBaseSchema = gameCardBaseSchema.merge(
|
||||
z.object({
|
||||
@@ -27,6 +30,8 @@ export const gameBaseSchema = gameCardBaseSchema.merge(
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
|
||||
genres: gameGenresSchema.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -36,15 +41,11 @@ export type GameCreateType = z.infer<typeof gameCreateSchema>;
|
||||
export const gameSchema = gameBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
owner_id: z.number().positive(),
|
||||
owner: ownerSchema,
|
||||
update_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
upload_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
})
|
||||
);
|
||||
export type GameType = z.infer<typeof gameSchema>;
|
||||
@@ -61,3 +62,7 @@ export const gamesSchema = z.array(z.any()).transform((a) => {
|
||||
});
|
||||
return games;
|
||||
});
|
||||
|
||||
export const isGame = (a: any): a is GameType => {
|
||||
return (a as GameType).type === TypesOfItems.game;
|
||||
};
|
||||
|
||||
@@ -34,7 +34,3 @@ export const gameCardsSchema = z.array(z.any()).transform((a) => {
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
|
||||
export const isGame = (a: any): a is GameCardType => {
|
||||
return (a as GameCardType).type === TypesOfItems.game;
|
||||
};
|
||||
|
||||
28
src/entities/item/game/schemas/genre.ts
Normal file
28
src/entities/item/game/schemas/genre.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const gameGenreBaseSchema = z.object({
|
||||
genre: z.string().min(3),
|
||||
});
|
||||
|
||||
export const gameGenreCreateSchema = gameGenreBaseSchema.merge(z.object({}));
|
||||
export type GameGenreCreateType = z.infer<typeof gameGenreCreateSchema>;
|
||||
|
||||
export const gameGenreSchema = gameGenreBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
})
|
||||
);
|
||||
export type GameGenreType = z.infer<typeof gameGenreSchema>;
|
||||
|
||||
export const isGameGenreStrict = (a: any): a is GameGenreType => {
|
||||
return gameGenreSchema.safeParse(a).success;
|
||||
};
|
||||
|
||||
export const gameGenresSchema = z.array(z.any()).transform((a) => {
|
||||
const cards: GameGenreType[] = [];
|
||||
a.forEach((e) => {
|
||||
if (isGameGenreStrict(e)) cards.push(gameGenreSchema.parse(e));
|
||||
else console.error("GameGenre parse error - ", e);
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
@@ -1,100 +1,37 @@
|
||||
import {
|
||||
gameSchema,
|
||||
gamesSchema,
|
||||
gameCreateSchema,
|
||||
type GameType,
|
||||
type GameCreateType,
|
||||
} from "./game/schemas/game";
|
||||
export {
|
||||
gameSchema,
|
||||
gamesSchema,
|
||||
gameCreateSchema,
|
||||
type GameType,
|
||||
type GameCreateType,
|
||||
};
|
||||
|
||||
import {
|
||||
gameCardSchema,
|
||||
gameCardsSchema,
|
||||
type GameCardType,
|
||||
isGame,
|
||||
} from "./game/schemas/gameCard";
|
||||
export { gameCardSchema, gameCardsSchema, type GameCardType, isGame };
|
||||
|
||||
import { GameService } from "./game/game";
|
||||
export { GameService };
|
||||
|
||||
import {
|
||||
movieSchema,
|
||||
moviesSchema,
|
||||
movieCreateSchema,
|
||||
type MovieType,
|
||||
type MovieCreateType,
|
||||
} from "./movie/schemas/movie";
|
||||
export {
|
||||
movieSchema,
|
||||
moviesSchema,
|
||||
movieCreateSchema,
|
||||
type MovieType,
|
||||
type MovieCreateType,
|
||||
};
|
||||
|
||||
import {
|
||||
movieCardSchema,
|
||||
movieCardsSchema,
|
||||
type MovieCardType,
|
||||
isMovie,
|
||||
} from "./movie/schemas/movieCard";
|
||||
export { movieCardSchema, movieCardsSchema, type MovieCardType, isMovie };
|
||||
import { isGame } from "./game/schemas/game";
|
||||
export { isGame };
|
||||
|
||||
import { MovieService } from "./movie/movie";
|
||||
export { MovieService };
|
||||
|
||||
import {
|
||||
audiobookSchema,
|
||||
audiobooksSchema,
|
||||
audiobookCreateSchema,
|
||||
type AudiobookType,
|
||||
type AudiobookCreateType,
|
||||
} from "./audiobook/schemas/audiobook";
|
||||
export {
|
||||
audiobookSchema,
|
||||
audiobooksSchema,
|
||||
audiobookCreateSchema,
|
||||
type AudiobookType,
|
||||
type AudiobookCreateType,
|
||||
};
|
||||
import { isMovie } from "./movie/schemas/movie";
|
||||
export { isMovie };
|
||||
|
||||
import {
|
||||
audiobookCardSchema,
|
||||
audiobookCardsSchema,
|
||||
type AudiobookCardType,
|
||||
isAudiobook,
|
||||
} from "./audiobook/schemas/audiobookCard";
|
||||
export {
|
||||
audiobookCardSchema,
|
||||
audiobookCardsSchema,
|
||||
type AudiobookCardType,
|
||||
isAudiobook,
|
||||
};
|
||||
|
||||
import { AudiobookService } from "./audiobook/audiobook";
|
||||
export { AudiobookService };
|
||||
import { isAudiobook } from "./audiobook/schemas/audiobook";
|
||||
export { isAudiobook };
|
||||
|
||||
import { ItemService } from "./item";
|
||||
export { ItemService };
|
||||
|
||||
import {
|
||||
TypesOfItems,
|
||||
isGenre,
|
||||
type IItemService,
|
||||
type ItemType,
|
||||
type ItemCardType,
|
||||
type ItemCreateType,
|
||||
type GenreType,
|
||||
type CreateGenreType,
|
||||
type ItemListPropertyType,
|
||||
} from "./types";
|
||||
export {
|
||||
TypesOfItems,
|
||||
isGenre,
|
||||
type IItemService,
|
||||
type ItemType,
|
||||
type ItemCardType,
|
||||
type ItemCreateType,
|
||||
type GenreType,
|
||||
type CreateGenreType,
|
||||
type ItemListPropertyType,
|
||||
};
|
||||
|
||||
@@ -10,6 +10,17 @@ import {
|
||||
TypesOfItems,
|
||||
} from "../types";
|
||||
import { ItemService } from "../item";
|
||||
import {
|
||||
MovieGenreCreateType,
|
||||
movieGenreSchema,
|
||||
movieGenresSchema,
|
||||
} from "./schemas/genre";
|
||||
import {
|
||||
MovieActorCreateType,
|
||||
movieActorSchema,
|
||||
movieActorsSchema,
|
||||
} from "./schemas/actors";
|
||||
import { RequiredFrom } from "@/shared/utils/types";
|
||||
|
||||
@staticImplements<IItemService>()
|
||||
export abstract class MovieService {
|
||||
@@ -26,7 +37,7 @@ export abstract class MovieService {
|
||||
|
||||
public static async GetCards() {
|
||||
return await HTTPService.get(
|
||||
`/${this.urlPrefix}/cards`,
|
||||
`/${this.urlPrefix}`,
|
||||
movieCardsSchema,
|
||||
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||
);
|
||||
@@ -49,7 +60,7 @@ export abstract class MovieService {
|
||||
});
|
||||
}
|
||||
|
||||
public static GetEmpty(): MovieCreateType {
|
||||
public static GetEmpty(): RequiredFrom<MovieCreateType> {
|
||||
return {
|
||||
title: "",
|
||||
torrent_file: "",
|
||||
@@ -57,6 +68,33 @@ export abstract class MovieService {
|
||||
};
|
||||
}
|
||||
|
||||
public static async GetGenres() {
|
||||
return await HTTPService.get(
|
||||
`/genres/${this.urlPrefix}`,
|
||||
movieGenresSchema
|
||||
);
|
||||
}
|
||||
|
||||
public static async CreateGenre(info: MovieGenreCreateType) {
|
||||
return await HTTPService.post(
|
||||
`/genres/${this.urlPrefix}`,
|
||||
movieGenreSchema,
|
||||
{
|
||||
body: info,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
public static async GetActors() {
|
||||
return await HTTPService.get(`/actors`, movieActorsSchema);
|
||||
}
|
||||
|
||||
public static async CreateActor(info: MovieActorCreateType) {
|
||||
return await HTTPService.post(`/actors`, movieActorSchema, {
|
||||
body: info,
|
||||
});
|
||||
}
|
||||
|
||||
public static propertiesDescription: ItemPropertiesDescriptionType<MovieType> =
|
||||
[
|
||||
[
|
||||
|
||||
28
src/entities/item/movie/schemas/actors.ts
Normal file
28
src/entities/item/movie/schemas/actors.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const movieActorBaseSchema = z.object({
|
||||
actor: z.string().min(3),
|
||||
});
|
||||
|
||||
export const movieActorCreateSchema = movieActorBaseSchema.merge(z.object({}));
|
||||
export type MovieActorCreateType = z.infer<typeof movieActorCreateSchema>;
|
||||
|
||||
export const movieActorSchema = movieActorBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
})
|
||||
);
|
||||
export type MovieActorType = z.infer<typeof movieActorSchema>;
|
||||
|
||||
export const isMovieActorStrict = (a: any): a is MovieActorType => {
|
||||
return movieActorSchema.safeParse(a).success;
|
||||
};
|
||||
|
||||
export const movieActorsSchema = z.array(z.any()).transform((a) => {
|
||||
const cards: MovieActorType[] = [];
|
||||
a.forEach((e) => {
|
||||
if (isMovieActorStrict(e)) cards.push(movieActorSchema.parse(e));
|
||||
else console.error("MovieActor parse error - ", e);
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
28
src/entities/item/movie/schemas/genre.ts
Normal file
28
src/entities/item/movie/schemas/genre.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const movieGenreBaseSchema = z.object({
|
||||
genre: z.string().min(3),
|
||||
});
|
||||
|
||||
export const movieGenreCreateSchema = movieGenreBaseSchema.merge(z.object({}));
|
||||
export type MovieGenreCreateType = z.infer<typeof movieGenreCreateSchema>;
|
||||
|
||||
export const movieGenreSchema = movieGenreBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
})
|
||||
);
|
||||
export type MovieGenreType = z.infer<typeof movieGenreSchema>;
|
||||
|
||||
export const isMovieGenreStrict = (a: any): a is MovieGenreType => {
|
||||
return movieGenreSchema.safeParse(a).success;
|
||||
};
|
||||
|
||||
export const movieGenresSchema = z.array(z.any()).transform((a) => {
|
||||
const cards: MovieGenreType[] = [];
|
||||
a.forEach((e) => {
|
||||
if (isMovieGenreStrict(e)) cards.push(movieGenreSchema.parse(e));
|
||||
else console.error("MovieGenre parse error - ", e);
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
@@ -1,5 +1,9 @@
|
||||
import { z } from "zod";
|
||||
import { movieCardBaseSchema } from "./movieCard";
|
||||
import { movieGenresSchema } from "./genre";
|
||||
import { movieActorsSchema } from "./actors";
|
||||
import { ownerSchema } from "../../schemas/owner";
|
||||
import { TypesOfItems } from "../../types";
|
||||
|
||||
export const movieBaseSchema = movieCardBaseSchema.merge(
|
||||
z.object({
|
||||
@@ -24,6 +28,9 @@ export const movieBaseSchema = movieCardBaseSchema.merge(
|
||||
})
|
||||
: undefined
|
||||
),
|
||||
|
||||
actors: movieActorsSchema.optional().nullable(),
|
||||
genres: movieGenresSchema.optional().nullable(),
|
||||
})
|
||||
);
|
||||
|
||||
@@ -33,15 +40,11 @@ export type MovieCreateType = z.infer<typeof movieCreateSchema>;
|
||||
export const movieSchema = movieBaseSchema.merge(
|
||||
z.object({
|
||||
id: z.number().positive(),
|
||||
owner_id: z.number().positive(),
|
||||
owner: ownerSchema,
|
||||
update_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
upload_date: z
|
||||
.string()
|
||||
.min(1)
|
||||
.transform((d) => new Date(d)),
|
||||
})
|
||||
);
|
||||
export type MovieType = z.infer<typeof movieSchema>;
|
||||
@@ -58,3 +61,7 @@ export const moviesSchema = z.array(z.any()).transform((a) => {
|
||||
});
|
||||
return games;
|
||||
});
|
||||
|
||||
export const isMovie = (a: any): a is MovieType => {
|
||||
return (a as MovieType).type === TypesOfItems.movie;
|
||||
};
|
||||
|
||||
@@ -34,7 +34,3 @@ export const movieCardsSchema = z.array(z.any()).transform((a) => {
|
||||
});
|
||||
return cards;
|
||||
});
|
||||
|
||||
export const isMovie = (a: any): a is MovieCardType => {
|
||||
return (a as MovieCardType).type === TypesOfItems.movie;
|
||||
};
|
||||
|
||||
7
src/entities/item/schemas/owner.ts
Normal file
7
src/entities/item/schemas/owner.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const ownerSchema = z.object({
|
||||
id: z.number().positive(),
|
||||
name: z.string().min(3),
|
||||
email: z.string().min(3),
|
||||
});
|
||||
@@ -7,6 +7,13 @@ import {
|
||||
AudiobookType,
|
||||
} from "./audiobook/schemas/audiobook";
|
||||
import { AudiobookCardType } from "./audiobook/schemas/audiobookCard";
|
||||
import { GameGenreCreateType, GameGenreType } from "./game/schemas/genre";
|
||||
import { MovieGenreCreateType, MovieGenreType } from "./movie/schemas/genre";
|
||||
import {
|
||||
AudiobookGenreCreateType,
|
||||
AudiobookGenreType,
|
||||
} from "./audiobook/schemas/genre";
|
||||
import { MovieActorType } from "./movie/schemas/actors";
|
||||
|
||||
export type ItemType = GameType | MovieType | AudiobookType;
|
||||
export type ItemCardType = GameCardType | MovieCardType | AudiobookCardType;
|
||||
@@ -14,9 +21,20 @@ export type ItemCreateType =
|
||||
| GameCreateType
|
||||
| MovieCreateType
|
||||
| AudiobookCreateType;
|
||||
|
||||
export type UnionItemType = GameType & MovieType & AudiobookType;
|
||||
|
||||
export type GenreType = GameGenreType | MovieGenreType | AudiobookGenreType;
|
||||
export type CreateGenreType =
|
||||
| GameGenreCreateType
|
||||
| MovieGenreCreateType
|
||||
| AudiobookGenreCreateType;
|
||||
|
||||
export type ItemListPropertyType = GenreType | MovieActorType;
|
||||
|
||||
export const isGenre = (g: any): g is GenreType => {
|
||||
return (g as GenreType).genre !== undefined;
|
||||
};
|
||||
|
||||
export enum TypesOfItems {
|
||||
game,
|
||||
movie,
|
||||
@@ -39,6 +57,8 @@ export interface IItemService {
|
||||
Change(id: number, info: ItemCreateType): Promise<ItemType | null>;
|
||||
GetEmpty(): ItemCreateType;
|
||||
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
|
||||
CreateGenre(info: CreateGenreType): Promise<GenreType | null>;
|
||||
GetGenres(): Promise<GenreType[] | null>;
|
||||
}
|
||||
export const staticImplements =
|
||||
<T>() =>
|
||||
|
||||
30
src/shared/assets/icons/cross.tsx
Normal file
30
src/shared/assets/icons/cross.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
export const CrossIcon = ({ className }: { className?: string }) => {
|
||||
return (
|
||||
<svg
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
className={className}
|
||||
>
|
||||
<g id="SVGRepo_bgCarrier" strokeWidth="0"></g>
|
||||
<g
|
||||
id="SVGRepo_tracerCarrier"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
></g>
|
||||
<g id="SVGRepo_iconCarrier">
|
||||
<path
|
||||
d="M6.99486 7.00636C6.60433 7.39689 6.60433 8.03005 6.99486 8.42058L10.58
|
||||
12.0057L6.99486 15.5909C6.60433 15.9814 6.60433 16.6146 6.99486
|
||||
17.0051C7.38538 17.3956 8.01855 17.3956 8.40907 17.0051L11.9942
|
||||
13.4199L15.5794 17.0051C15.9699 17.3956 16.6031 17.3956 16.9936
|
||||
17.0051C17.3841 16.6146 17.3841 15.9814 16.9936 15.5909L13.4084
|
||||
12.0057L16.9936 8.42059C17.3841 8.03007 17.3841 7.3969 16.9936
|
||||
7.00638C16.603 6.61585 15.9699 6.61585 15.5794 7.00638L11.9942
|
||||
10.5915L8.40907 7.00636C8.01855 6.61584 7.38538 6.61584 6.99486 7.00636Z"
|
||||
fill="var(--color-bg1)"
|
||||
></path>{" "}
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
@@ -2,5 +2,6 @@ import { SearchIcon } from "./searchIcon";
|
||||
import { PersonIcon } from "./personIcon";
|
||||
import { SunIcon } from "./sunIcon";
|
||||
import { SpinnerIcon } from "./spinnerIcon";
|
||||
import { CrossIcon } from "./cross";
|
||||
|
||||
export { SearchIcon, PersonIcon, SunIcon, SpinnerIcon };
|
||||
export { SearchIcon, PersonIcon, SunIcon, SpinnerIcon, CrossIcon };
|
||||
|
||||
@@ -8,7 +8,7 @@ export const Img = ({
|
||||
alt,
|
||||
className,
|
||||
}: {
|
||||
src: string;
|
||||
src: string | null | undefined;
|
||||
preview?: boolean;
|
||||
width?: number;
|
||||
height?: number;
|
||||
@@ -16,18 +16,22 @@ export const Img = ({
|
||||
className: string;
|
||||
}) => {
|
||||
return (
|
||||
<Image
|
||||
className={className}
|
||||
src={
|
||||
(preview
|
||||
? process.env.NEXT_PUBLIC_COVER_PREVIEW_URL
|
||||
: process.env.NEXT_PUBLIC_COVER_FULL_URL) +
|
||||
"/" +
|
||||
src
|
||||
}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt ?? ""}
|
||||
/>
|
||||
<>
|
||||
{src && (
|
||||
<Image
|
||||
className={className}
|
||||
src={
|
||||
(preview
|
||||
? process.env.NEXT_PUBLIC_COVER_PREVIEW_URL
|
||||
: process.env.NEXT_PUBLIC_COVER_FULL_URL) +
|
||||
"/" +
|
||||
src
|
||||
}
|
||||
width={width}
|
||||
height={height}
|
||||
alt={alt ?? ""}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -17,6 +17,7 @@ type RequestOptions = GetRequestOptions & {
|
||||
|
||||
export abstract class HTTPService {
|
||||
private static deepUndefinedToNull(o?: object): object | undefined {
|
||||
if (Array.isArray(o)) return o;
|
||||
if (o)
|
||||
return Object.fromEntries(
|
||||
Object.entries(o).map(([k, v]) => {
|
||||
|
||||
7
src/shared/utils/types.ts
Normal file
7
src/shared/utils/types.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
type RequiredKeys<T> = {
|
||||
[K in keyof T]-?: {} extends Pick<T, K> ? never : K;
|
||||
}[keyof T];
|
||||
|
||||
export type RequiredFrom<T> = {
|
||||
[K in RequiredKeys<T>]-?: T[K];
|
||||
};
|
||||
@@ -13,7 +13,10 @@ export const MobileMenu = () => {
|
||||
<button
|
||||
className="w-16 h-16 *:w-12 *:h-1 *:bg-fg1 *:my-3
|
||||
*:transition-all *:duration-300 *:relative"
|
||||
onClick={() => changeMenuOpen(!open)}
|
||||
onClick={(e) => {
|
||||
changeMenuOpen(!open);
|
||||
e.stopPropagation();
|
||||
}}
|
||||
onBlur={() => changeMenuOpen(false)}
|
||||
>
|
||||
<div
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { FilesService } from "@/entities/files";
|
||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||
import { ItemCreateType } from "@/entities/item";
|
||||
import { Img } from "@/shared/ui";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UseFormSetValue } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
export const ItemCover = ({
|
||||
cover,
|
||||
editable,
|
||||
setFormValue: setValue,
|
||||
}: {
|
||||
cover: string | null | undefined;
|
||||
editable: boolean;
|
||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||
}) => {
|
||||
const { register, setValue, watch } = useFormContext<ItemCreateType>();
|
||||
|
||||
const watch_cover = watch("cover");
|
||||
|
||||
useEffect(() => {
|
||||
register("cover", { value: cover });
|
||||
}, [cover, register]);
|
||||
|
||||
const onCoverDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
@@ -48,18 +54,16 @@ export const ItemCover = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
{(cover || editable) && (
|
||||
{(cover || watch_cover || editable) && (
|
||||
<div className="lp:w-[60%] lp:px-4 lp:pl-0 pt-2 pb-4 float-left relative">
|
||||
{cover && (
|
||||
<Img
|
||||
src={cover}
|
||||
preview={false}
|
||||
className="transition-all rounded-lg w-full object-contain"
|
||||
width={1280}
|
||||
height={720}
|
||||
/>
|
||||
)}
|
||||
{!cover && editable && (
|
||||
<Img
|
||||
src={watch_cover ?? cover}
|
||||
preview={false}
|
||||
className="transition-all rounded-lg w-full object-contain"
|
||||
width={1280}
|
||||
height={720}
|
||||
/>
|
||||
{!watch_cover && !cover && editable && (
|
||||
<div className="w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
||||
)}
|
||||
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
||||
import {
|
||||
useFormContext,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
} from "react-hook-form";
|
||||
import clsx from "clsx";
|
||||
import { SpinnerIcon } from "@/shared/assets/icons";
|
||||
|
||||
@@ -8,23 +12,22 @@ export const ItemDetails = ({
|
||||
description,
|
||||
editable,
|
||||
state,
|
||||
registerFormField: register,
|
||||
setFormValue: setValue,
|
||||
}: {
|
||||
title: {
|
||||
title: string;
|
||||
default_title: string;
|
||||
error: string | undefined;
|
||||
};
|
||||
description: {
|
||||
description: string | null | undefined;
|
||||
default_description: string | null | undefined;
|
||||
};
|
||||
title: string;
|
||||
description: string | null | undefined;
|
||||
editable: boolean;
|
||||
state: "saved" | "editing" | "error";
|
||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||
}) => {
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<ItemCreateType>();
|
||||
|
||||
const watched_title = watch("title");
|
||||
const watched_description = watch("description");
|
||||
|
||||
return (
|
||||
<span>
|
||||
<span className="flex items-end justify-between relative pt-2">
|
||||
@@ -32,7 +35,7 @@ export const ItemDetails = ({
|
||||
<span
|
||||
className={clsx(
|
||||
"text-fg4 text-2xl absolute -z-10 opacity-0",
|
||||
title.title === "" && "opacity-100",
|
||||
watched_title === "" && "opacity-100",
|
||||
"transition-opacity cursor-text"
|
||||
)}
|
||||
>
|
||||
@@ -46,7 +49,7 @@ export const ItemDetails = ({
|
||||
)}
|
||||
suppressContentEditableWarning={true}
|
||||
contentEditable={editable}
|
||||
{...register("title", { value: title.default_title })}
|
||||
{...register("title", { value: title })}
|
||||
onInput={(e) => {
|
||||
setValue("title", e.currentTarget.innerText, {
|
||||
shouldValidate: true,
|
||||
@@ -54,7 +57,7 @@ export const ItemDetails = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
{title.default_title}
|
||||
{title}
|
||||
</h1>
|
||||
|
||||
{editable && (
|
||||
@@ -73,14 +76,14 @@ export const ItemDetails = ({
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
<div className="text-err text-xs w-full h-2">{title.error}</div>
|
||||
{(description.default_description || editable) && (
|
||||
<div className="text-err text-xs w-full h-2">{errors.title?.message}</div>
|
||||
{(description || editable) && (
|
||||
<span className="relative">
|
||||
{editable && (
|
||||
<span
|
||||
className={clsx(
|
||||
"text-fg4 text-md absolute -z-10 opacity-0",
|
||||
(description.description === "" || description === undefined) &&
|
||||
(watched_description === "" || description === undefined) &&
|
||||
"opacity-100",
|
||||
"transition-opacity mt-2"
|
||||
)}
|
||||
@@ -97,7 +100,7 @@ export const ItemDetails = ({
|
||||
!editable && "cursor-default"
|
||||
)}
|
||||
{...register("description", {
|
||||
value: description.default_description,
|
||||
value: description,
|
||||
})}
|
||||
onInput={(e) => {
|
||||
setValue("description", e.currentTarget.innerText, {
|
||||
@@ -106,7 +109,7 @@ export const ItemDetails = ({
|
||||
});
|
||||
}}
|
||||
>
|
||||
{description.default_description}
|
||||
{description}
|
||||
</div>
|
||||
</span>
|
||||
)}
|
||||
|
||||
@@ -1,21 +1,25 @@
|
||||
import { FilesService } from "@/entities/files";
|
||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||
import { useCallback } from "react";
|
||||
import { ItemCreateType } from "@/entities/item";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const ItemFragment = ({
|
||||
fragment,
|
||||
editable,
|
||||
setFormValue: setValue,
|
||||
registerFormField: register,
|
||||
}: {
|
||||
fragment: string | undefined | null;
|
||||
editable: boolean;
|
||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
||||
}) => {
|
||||
const { register, setValue, watch } = useFormContext<ItemCreateType>();
|
||||
|
||||
const watched_fragment = watch("fragment");
|
||||
|
||||
useEffect(() => {
|
||||
register("fragment", { value: fragment });
|
||||
}, [fragment, register]);
|
||||
|
||||
const onFragmentDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
@@ -57,8 +61,15 @@ export const ItemFragment = ({
|
||||
controls
|
||||
controlsList="nodownload"
|
||||
typeof="audio/mpeg"
|
||||
src={process.env.NEXT_PUBLIC_FRAGMENT_URL + "/" + (fragment ?? "")}
|
||||
className={clsx(!fragment && "pointer-events-none", "w-full h-full")}
|
||||
src={
|
||||
process.env.NEXT_PUBLIC_FRAGMENT_URL +
|
||||
"/" +
|
||||
(watched_fragment ?? "")
|
||||
}
|
||||
className={clsx(
|
||||
!watched_fragment && "pointer-events-none",
|
||||
"w-full h-full"
|
||||
)}
|
||||
/>
|
||||
{editable && (
|
||||
<>
|
||||
|
||||
@@ -6,22 +6,24 @@ import {
|
||||
isMovie,
|
||||
ItemCreateType,
|
||||
ItemType,
|
||||
MovieService,
|
||||
} from "@/entities/item";
|
||||
import { UserService } from "@/entities/user";
|
||||
import useSWR from "swr";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { ItemCover } from "./itemCover";
|
||||
import { ItemService } from "@/entities/item/item";
|
||||
import { ItemService as IS } from "@/entities/item/item";
|
||||
import { ItemProperties } from "./itemProperties";
|
||||
import { ItemTrailer } from "./itemTrailer";
|
||||
import { ItemTorrent } from "./itemTorrent";
|
||||
import { ItemDetails } from "./itemDetails";
|
||||
import { ItemFragment } from "./itemFragment";
|
||||
import { EraseCacheByTag } from "@/shared/utils/http";
|
||||
import { ItemListProperties } from "./itemListProperties";
|
||||
import { RequiredFrom } from "@/shared/utils/types";
|
||||
|
||||
export const ItemInfo = <T extends ItemType | ItemCreateType>({
|
||||
export const ItemInfo = <T extends ItemType | RequiredFrom<ItemCreateType>>({
|
||||
item: init_item,
|
||||
}: {
|
||||
item: T;
|
||||
@@ -33,140 +35,118 @@ export const ItemInfo = <T extends ItemType | ItemCreateType>({
|
||||
|
||||
useEffect(() => {
|
||||
if (me) {
|
||||
if (ItemService.isExistingItem(item))
|
||||
setEditable(me.id === item.owner_id);
|
||||
if (IS.isExistingItem(item)) setEditable(me.id === item.owner.id);
|
||||
else setEditable(true);
|
||||
}
|
||||
}, [me, item]);
|
||||
|
||||
const formRef = useRef<HTMLFormElement>(null);
|
||||
const {
|
||||
register,
|
||||
handleSubmit,
|
||||
setValue,
|
||||
setError,
|
||||
watch,
|
||||
reset,
|
||||
formState: { dirtyFields, errors },
|
||||
} = useForm<ItemType | ItemCreateType>({
|
||||
const form = useForm<ItemCreateType>({
|
||||
defaultValues: init_item,
|
||||
resolver: zodResolver(
|
||||
ItemService.itemsConfiguration[item.type].formResolver
|
||||
),
|
||||
resolver: zodResolver(IS.itemsConfiguration[item.type].formResolver),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
register("torrent_file", { value: item.torrent_file });
|
||||
register("cover", { value: item.cover });
|
||||
if (isAudiobook(item)) register("fragment", { value: item.fragment });
|
||||
}, [item.cover, item.torrent_file, register]);
|
||||
|
||||
const [savedTimeout, changeSavedTimeout] = useState<NodeJS.Timeout | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const watchedData = watch();
|
||||
const watch = form.watch();
|
||||
|
||||
const [formData, changeFormData] = useState<T | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!Object.keys(dirtyFields).length) return;
|
||||
if (JSON.stringify(watchedData) === JSON.stringify(formData)) return;
|
||||
changeFormData(watchedData as T);
|
||||
if (!Object.keys(form.formState.dirtyFields).length) return;
|
||||
if (JSON.stringify(watch) === JSON.stringify(formData)) return;
|
||||
changeFormData(watch as T);
|
||||
if (savedTimeout) clearTimeout(savedTimeout);
|
||||
changeSavedTimeout(
|
||||
setTimeout(() => {
|
||||
if (formRef.current) formRef.current.requestSubmit();
|
||||
}, 3000)
|
||||
);
|
||||
}, [watchedData]);
|
||||
}, [watch]);
|
||||
|
||||
const onSubmit = async (formData: ItemCreateType) => {
|
||||
const updatedItem = ItemService.isExistingItem(item)
|
||||
? await ItemService.ChangeItem(item.id, formData)
|
||||
: await ItemService.AddItem(formData);
|
||||
const updatedItem = IS.isExistingItem(item)
|
||||
? await IS.ChangeItem(item.id, formData)
|
||||
: await IS.AddItem(formData);
|
||||
changeSavedTimeout(null);
|
||||
if (updatedItem) {
|
||||
changeItem(updatedItem as T);
|
||||
reset({}, { keepValues: true });
|
||||
form.reset({}, { keepValues: true });
|
||||
} else {
|
||||
setError("root", { message: "Ошибка сервера" });
|
||||
form.setError("root", { message: "Ошибка сервера" });
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => console.log(errors), [errors]);
|
||||
useEffect(() => console.log(form.formState.errors), [form.formState.errors]);
|
||||
|
||||
return (
|
||||
<form
|
||||
onSubmit={handleSubmit(onSubmit)}
|
||||
className="m-4 flex flex-col lp:block"
|
||||
ref={formRef}
|
||||
>
|
||||
<ItemCover
|
||||
cover={watchedData.cover}
|
||||
editable={editable}
|
||||
setFormValue={setValue}
|
||||
/>
|
||||
<FormProvider {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="m-4 flex flex-col lp:block"
|
||||
ref={formRef}
|
||||
>
|
||||
<ItemCover
|
||||
cover={IS.isExistingItem(item) ? item.cover : null}
|
||||
editable={editable}
|
||||
/>
|
||||
|
||||
<ItemDetails
|
||||
title={{
|
||||
title: watchedData.title,
|
||||
default_title: item.title,
|
||||
error: errors.title?.message,
|
||||
}}
|
||||
description={{
|
||||
description: watchedData.description,
|
||||
default_description: item.description,
|
||||
}}
|
||||
editable={editable}
|
||||
state={
|
||||
savedTimeout
|
||||
? Object.keys(errors).length > 0
|
||||
? "error"
|
||||
: "editing"
|
||||
: "saved"
|
||||
}
|
||||
registerFormField={register}
|
||||
setFormValue={setValue}
|
||||
/>
|
||||
<ItemDetails
|
||||
title={item.title}
|
||||
description={IS.isExistingItem(item) ? item.description : null}
|
||||
editable={editable}
|
||||
state={
|
||||
savedTimeout
|
||||
? Object.keys(form.formState.errors).length > 0
|
||||
? "error"
|
||||
: "editing"
|
||||
: "saved"
|
||||
}
|
||||
/>
|
||||
|
||||
<ItemProperties
|
||||
item={item}
|
||||
watchedFormData={watchedData}
|
||||
editable={editable}
|
||||
setFormValue={setValue}
|
||||
registerFormField={register}
|
||||
/>
|
||||
<ItemListProperties
|
||||
propertyName="genres"
|
||||
propertyList={IS.isExistingItem(item) ? item.genres : null}
|
||||
editable={editable}
|
||||
getAllTags={async () =>
|
||||
await IS.itemsConfiguration[item.type].service.GetGenres()
|
||||
}
|
||||
createTag={async (property: string) =>
|
||||
await IS.itemsConfiguration[item.type].service.CreateGenre({
|
||||
genre: property,
|
||||
})
|
||||
}
|
||||
/>
|
||||
|
||||
{(isGame(item) || isMovie(item)) &&
|
||||
(isGame(watchedData) || isMovie(watchedData)) && (
|
||||
<ItemTrailer
|
||||
default_trailer={item.trailer}
|
||||
trailer={watchedData.trailer}
|
||||
<ItemProperties item={item} editable={editable} />
|
||||
|
||||
{isMovie(item) && (
|
||||
<ItemListProperties
|
||||
propertyName="actors"
|
||||
propertyList={IS.isExistingItem(item) ? item.actors : null}
|
||||
editable={editable}
|
||||
registerFormField={register}
|
||||
setFormValue={setValue}
|
||||
getAllTags={async () => await MovieService.GetActors()}
|
||||
createTag={async (property: string) =>
|
||||
await MovieService.CreateActor({ actor: property })
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{isAudiobook(watchedData) && (
|
||||
<ItemFragment
|
||||
fragment={watchedData.fragment}
|
||||
editable={editable}
|
||||
registerFormField={register}
|
||||
setFormValue={setValue}
|
||||
/>
|
||||
)}
|
||||
{((isGame(item) && isGame(watch)) ||
|
||||
(isMovie(item) && isMovie(watch))) && (
|
||||
<ItemTrailer default_trailer={item.trailer} editable={editable} />
|
||||
)}
|
||||
|
||||
<ItemTorrent
|
||||
title={watchedData.title}
|
||||
torrent_file={watchedData.torrent_file}
|
||||
editable={editable}
|
||||
error={errors.torrent_file?.message}
|
||||
setFormValue={setValue}
|
||||
/>
|
||||
{isAudiobook(item) && isAudiobook(watch) && (
|
||||
<ItemFragment fragment={item.fragment} editable={editable} />
|
||||
)}
|
||||
|
||||
<input type="submit" className="hidden" />
|
||||
</form>
|
||||
<ItemTorrent torrent_file={item.torrent_file} editable={editable} />
|
||||
|
||||
<input type="submit" className="hidden" />
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
126
src/widgets/itemInfo/itemListProperties.tsx
Normal file
126
src/widgets/itemInfo/itemListProperties.tsx
Normal file
@@ -0,0 +1,126 @@
|
||||
import { isGenre, ItemCreateType, ItemListPropertyType } from "@/entities/item";
|
||||
import { CrossIcon } from "@/shared/assets/icons";
|
||||
import clsx from "clsx";
|
||||
import { useEffect, useState } from "react";
|
||||
import { FieldArrayPath, useFieldArray, useFormContext } from "react-hook-form";
|
||||
|
||||
export const ItemListProperties = <T extends ItemListPropertyType>({
|
||||
propertyName,
|
||||
editable,
|
||||
getAllTags,
|
||||
createTag,
|
||||
}: {
|
||||
propertyName: FieldArrayPath<ItemCreateType>;
|
||||
propertyList: T[] | null | undefined;
|
||||
editable: boolean;
|
||||
getAllTags: () => Promise<T[] | null>;
|
||||
createTag: (property: string) => Promise<T | null>;
|
||||
}) => {
|
||||
const { watch } = useFormContext<ItemCreateType>();
|
||||
|
||||
const watched_property = watch(propertyName);
|
||||
|
||||
const { fields, append, replace, remove } = useFieldArray<ItemCreateType>({
|
||||
name: propertyName,
|
||||
});
|
||||
|
||||
const [allTags, setAllTags] = useState<T[] | null>();
|
||||
const [searchTags, setSearchTags] = useState<T[] | null>();
|
||||
const [addition, changeAdditionState] = useState<boolean>(false);
|
||||
|
||||
const getTagValue = (tag: T) => (isGenre(tag) ? tag.genre : tag.actor);
|
||||
const getTagsByQuery = (query: string) =>
|
||||
allTags
|
||||
?.filter((t) => getTagValue(t).includes(query))
|
||||
.filter((t) => !watched_property?.map((tag) => tag.id).includes(t.id));
|
||||
|
||||
const [searchField, changeSearchField] = useState<string>("");
|
||||
|
||||
useEffect(() => setSearchTags(getTagsByQuery(searchField)), [searchField]);
|
||||
useEffect(
|
||||
() => console.log(fields, watched_property),
|
||||
[fields, watched_property]
|
||||
);
|
||||
|
||||
return (
|
||||
<span>
|
||||
<div className="flex items-start justify-start p-2 flex-wrap">
|
||||
{fields.map((field, i) => (
|
||||
<div
|
||||
className="px-2 py-1 bg-bg1 rounded-lg m-1 relative group cursor-default"
|
||||
key={field.id}
|
||||
>
|
||||
{watched_property && (
|
||||
<span>{getTagValue(watched_property[i] as T)}</span>
|
||||
)}
|
||||
{editable && (
|
||||
<button
|
||||
className="opacity-0 group-hover:opacity-100 transition-opacity
|
||||
cursor-pointer absolute -top-1 -right-1 rounded-full bg-bg4 w-4 h-4"
|
||||
onClick={() => remove(i)}
|
||||
>
|
||||
<CrossIcon />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{editable && (
|
||||
<>
|
||||
<button
|
||||
className={clsx(
|
||||
"px-2 py-1 bg-bg1 rounded-lg m-1 cursor-pointer transition-opacity",
|
||||
addition && "opacity-0"
|
||||
)}
|
||||
onClick={async () => {
|
||||
changeAdditionState(editable);
|
||||
setAllTags(await getAllTags());
|
||||
}}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
<div
|
||||
className={clsx(
|
||||
"px-2 py-1 bg-bg1 rounded-lg m-1 opacity-0 transition-all relative",
|
||||
!addition && "w-0 h-0 overflow-hidden",
|
||||
addition && "opacity-100 !h-fit"
|
||||
)}
|
||||
>
|
||||
<input
|
||||
className="w-24 outline-none"
|
||||
onChange={(e) => changeSearchField(e.target.value)}
|
||||
onKeyDown={async (e) => {
|
||||
if (e.key === "Enter") {
|
||||
const tag = await createTag(searchField);
|
||||
if (tag) replace([...(watched_property ?? []), tag]);
|
||||
changeAdditionState(false);
|
||||
}
|
||||
}}
|
||||
></input>
|
||||
<div
|
||||
className={clsx(
|
||||
"static w-28 bg-bg1 top-0 left-0 px-2 opacity-0",
|
||||
"transition-opacity rounded-b-lg",
|
||||
searchTags && "opacity-100"
|
||||
)}
|
||||
>
|
||||
{searchTags?.map((tag) => (
|
||||
<div
|
||||
key={tag.id}
|
||||
className="w-full py-1 cursor-pointer overflow-hidden text-ellipsis"
|
||||
onClick={() => {
|
||||
append(tag);
|
||||
console.log("updated");
|
||||
changeAdditionState(false);
|
||||
}}
|
||||
>
|
||||
{getTagValue(tag)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
@@ -1,22 +1,27 @@
|
||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||
import { ItemService } from "@/entities/item/item";
|
||||
import { ItemPropertiesDescriptionType } from "@/entities/item/types";
|
||||
import { RequiredFrom } from "@/shared/utils/types";
|
||||
import clsx from "clsx";
|
||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
||||
import {
|
||||
useFormContext,
|
||||
UseFormRegister,
|
||||
UseFormSetValue,
|
||||
} from "react-hook-form";
|
||||
|
||||
export const ItemProperties = <T extends ItemType | ItemCreateType>({
|
||||
export const ItemProperties = <
|
||||
T extends ItemType | RequiredFrom<ItemCreateType>
|
||||
>({
|
||||
item,
|
||||
watchedFormData: watchedData,
|
||||
editable,
|
||||
setFormValue: setValue,
|
||||
registerFormField: register,
|
||||
}: {
|
||||
item: T; // Init values
|
||||
watchedFormData: T; // Updated values
|
||||
editable: boolean;
|
||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
||||
}) => {
|
||||
const { register, setValue, watch } = useFormContext<ItemCreateType>();
|
||||
|
||||
const watchedData = watch();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={clsx(
|
||||
@@ -26,7 +31,7 @@ export const ItemProperties = <T extends ItemType | ItemCreateType>({
|
||||
>
|
||||
{(
|
||||
ItemService.itemsConfiguration[item.type]
|
||||
.propertiesDescription as ItemPropertiesDescriptionType<T>
|
||||
.propertiesDescription as ItemPropertiesDescriptionType<ItemCreateType>
|
||||
).map((section, i) => (
|
||||
<ul key={i} className="w-[48%] bg-bg1 rounded-lg py-1 px-4">
|
||||
{section.map((req) => (
|
||||
@@ -54,27 +59,25 @@ export const ItemProperties = <T extends ItemType | ItemCreateType>({
|
||||
(watchedData[req.key] as string) === "") &&
|
||||
"opacity-100 absolute left-0 top-0 inline-block min-w-10 z-10"
|
||||
)}
|
||||
{...register(req.key as keyof ItemType, {
|
||||
{...register(req.key, {
|
||||
value: req.value
|
||||
? req.value(item)
|
||||
: undefined ?? (item[req.key] as string),
|
||||
? req.value(item as ItemCreateType)
|
||||
: undefined ??
|
||||
((item as ItemCreateType)[req.key] as string),
|
||||
})}
|
||||
contentEditable={editable && (req.editable ?? true)}
|
||||
suppressContentEditableWarning={true}
|
||||
onInput={(e) => {
|
||||
setValue(
|
||||
req.key as keyof ItemType,
|
||||
e.currentTarget.innerText,
|
||||
{
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
}
|
||||
);
|
||||
setValue(req.key, e.currentTarget.innerText, {
|
||||
shouldValidate: true,
|
||||
shouldDirty: true,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{req.value
|
||||
? req.value(item)
|
||||
: undefined ?? (item[req.key] as string)}
|
||||
? req.value(item as ItemCreateType)
|
||||
: undefined ??
|
||||
((item as ItemCreateType)[req.key] as string)}
|
||||
</span>
|
||||
</span>
|
||||
</li>
|
||||
|
||||
@@ -1,24 +1,32 @@
|
||||
import { FilesService } from "@/entities/files";
|
||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||
import Link from "next/link";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import { UseFormSetValue } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
import clsx from "clsx";
|
||||
|
||||
export const ItemTorrent = ({
|
||||
title,
|
||||
torrent_file,
|
||||
editable,
|
||||
error,
|
||||
setFormValue: setValue,
|
||||
}: {
|
||||
title: string;
|
||||
torrent_file: string;
|
||||
editable: boolean;
|
||||
error: string | undefined;
|
||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||
}) => {
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
watch,
|
||||
formState: { errors },
|
||||
} = useFormContext<ItemCreateType>();
|
||||
|
||||
const watched_torrent_file = watch("torrent_file");
|
||||
const watched_title = watch("title");
|
||||
|
||||
useEffect(() => {
|
||||
register("torrent_file", { value: torrent_file });
|
||||
}, [torrent_file, register]);
|
||||
|
||||
const onTorrentDrop = useCallback(
|
||||
(acceptedFiles: File[]) => {
|
||||
const file = acceptedFiles[0];
|
||||
@@ -58,21 +66,23 @@ export const ItemTorrent = ({
|
||||
>
|
||||
<div className="flex flex-col items-center">
|
||||
<Link
|
||||
href={process.env.NEXT_PUBLIC_CONTENT_URL + "/" + torrent_file}
|
||||
href={
|
||||
process.env.NEXT_PUBLIC_CONTENT_URL + "/" + watched_torrent_file
|
||||
}
|
||||
className={clsx(
|
||||
"p-4 bg-ac0 text-fg1 text-2xl rounded-lg",
|
||||
!torrent_file && "bg-bg1 text-fg4"
|
||||
!watched_torrent_file && "bg-bg1 text-fg4"
|
||||
)}
|
||||
>
|
||||
Скачать {title}
|
||||
Скачать {watched_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">
|
||||
{error && (
|
||||
<span className="w-full text-center text-err">{error}</span>
|
||||
)}
|
||||
<span className="w-full text-center text-err">
|
||||
{errors.torrent_file?.message}
|
||||
</span>
|
||||
{isTorrentDragActive ? (
|
||||
<span className="w-full text-center">
|
||||
Изменить .torrent файл...
|
||||
|
||||
@@ -1,32 +1,30 @@
|
||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||
import { ItemCreateType } from "@/entities/item";
|
||||
import { getYouTubeID } from "@/shared/utils/getYoutubeId";
|
||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
||||
import { useFormContext } from "react-hook-form";
|
||||
|
||||
export const ItemTrailer = ({
|
||||
trailer,
|
||||
default_trailer,
|
||||
editable,
|
||||
setFormValue: setValue,
|
||||
registerFormField: register,
|
||||
}: {
|
||||
trailer: string | undefined | null;
|
||||
default_trailer: string | undefined | null;
|
||||
editable: boolean;
|
||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
||||
}) => {
|
||||
const { register, setValue, watch } = useFormContext<ItemCreateType>();
|
||||
|
||||
const watched_trailer = watch("trailer");
|
||||
|
||||
return (
|
||||
<>
|
||||
{(trailer || editable) && (
|
||||
{(watched_trailer || editable) && (
|
||||
<div className="w-ful aspect-video">
|
||||
{trailer && getYouTubeID(trailer) && (
|
||||
{watched_trailer && getYouTubeID(watched_trailer) && (
|
||||
<iframe
|
||||
src={"https://youtube.com/embed/" + getYouTubeID(trailer)}
|
||||
src={"https://youtube.com/embed/" + getYouTubeID(watched_trailer)}
|
||||
className="w-full aspect-video rounded-lg mt-4"
|
||||
allowFullScreen
|
||||
/>
|
||||
)}
|
||||
{!trailer && editable && (
|
||||
{!watched_trailer && editable && (
|
||||
<div className="mt-4 w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
||||
)}
|
||||
</div>
|
||||
@@ -42,7 +40,7 @@ export const ItemTrailer = ({
|
||||
setValue("trailer", e.target.value);
|
||||
},
|
||||
})}
|
||||
defaultValue={default_trailer}
|
||||
defaultValue={default_trailer ?? ""}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user