mirror of
https://github.com/StepanovPlaton/torrent_frontend.git
synced 2026-04-03 20:30:48 +04:00
Add genres and actors
This commit is contained in:
@@ -9,9 +9,9 @@
|
|||||||
- Next.js 14 (App Router)
|
- Next.js 14 (App Router)
|
||||||
- Tailwind CSS
|
- Tailwind CSS
|
||||||
- Zod
|
- Zod
|
||||||
|
- React Hook Form
|
||||||
- SWR
|
- SWR
|
||||||
- clsx
|
- clsx
|
||||||
- React Hook Form
|
|
||||||
- и другие
|
- и другие
|
||||||
- next-themes
|
- next-themes
|
||||||
- js-cookie
|
- js-cookie
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export default function Login() {
|
|||||||
>
|
>
|
||||||
<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 outline-none"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export default function Registration() {
|
|||||||
>
|
>
|
||||||
<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 outline-none"
|
||||||
placeholder=" "
|
placeholder=" "
|
||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { isSection, ItemService, MovieService } from "@/entities/item";
|
import { ItemService } from "@/entities/item";
|
||||||
import { ItemCard } from "@/widgets/itemCard";
|
import { ItemCard } from "@/widgets/itemCard";
|
||||||
import { Section } from "@/widgets/section";
|
import { Section } from "@/widgets/section";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ import {
|
|||||||
TypesOfItems,
|
TypesOfItems,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { ItemService } from "../item";
|
import { ItemService } from "../item";
|
||||||
|
import {
|
||||||
|
AudiobookGenreCreateType,
|
||||||
|
audiobookGenreSchema,
|
||||||
|
audiobookGenresSchema,
|
||||||
|
} from "./schemas/genre";
|
||||||
|
import { RequiredFrom } from "@/shared/utils/types";
|
||||||
|
|
||||||
@staticImplements<IItemService>()
|
@staticImplements<IItemService>()
|
||||||
export abstract class AudiobookService {
|
export abstract class AudiobookService {
|
||||||
@@ -30,7 +36,7 @@ export abstract class AudiobookService {
|
|||||||
|
|
||||||
public static async GetCards() {
|
public static async GetCards() {
|
||||||
return await HTTPService.get(
|
return await HTTPService.get(
|
||||||
`/${this.urlPrefix}/cards`,
|
`/${this.urlPrefix}`,
|
||||||
audiobookCardsSchema,
|
audiobookCardsSchema,
|
||||||
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||||
);
|
);
|
||||||
@@ -53,7 +59,7 @@ export abstract class AudiobookService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetEmpty(): AudiobookCreateType {
|
public static GetEmpty(): RequiredFrom<AudiobookCreateType> {
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
torrent_file: "",
|
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> = [
|
static propertiesDescription: ItemPropertiesDescriptionType<AudiobookType> = [
|
||||||
[
|
[
|
||||||
{ name: "Автор", key: "author" },
|
{ name: "Автор", key: "author" },
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { audiobookCardBaseSchema } from "./audiobookCard";
|
import { audiobookCardBaseSchema } from "./audiobookCard";
|
||||||
|
import { audiobookGenresSchema } from "./genre";
|
||||||
|
import { ownerSchema } from "../../schemas/owner";
|
||||||
|
import { TypesOfItems } from "../../types";
|
||||||
|
|
||||||
export const audiobookBaseSchema = audiobookCardBaseSchema.merge(
|
export const audiobookBaseSchema = audiobookCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -22,6 +25,8 @@ export const audiobookBaseSchema = audiobookCardBaseSchema.merge(
|
|||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
),
|
),
|
||||||
|
|
||||||
|
genres: audiobookGenresSchema.optional().nullable(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -31,15 +36,11 @@ export type AudiobookCreateType = z.infer<typeof audiobookCreateSchema>;
|
|||||||
export const audiobookSchema = audiobookBaseSchema.merge(
|
export const audiobookSchema = audiobookBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().positive(),
|
id: z.number().positive(),
|
||||||
owner_id: z.number().positive(),
|
owner: ownerSchema,
|
||||||
update_date: z
|
update_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
upload_date: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.transform((d) => new Date(d)),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
export type AudiobookType = z.infer<typeof audiobookSchema>;
|
export type AudiobookType = z.infer<typeof audiobookSchema>;
|
||||||
@@ -56,3 +57,7 @@ export const audiobooksSchema = z.array(z.any()).transform((a) => {
|
|||||||
});
|
});
|
||||||
return audiobooks;
|
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;
|
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,
|
TypesOfItems,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { ItemService } from "../item";
|
import { ItemService } from "../item";
|
||||||
|
import {
|
||||||
|
GameGenreCreateType,
|
||||||
|
gameGenreSchema,
|
||||||
|
gameGenresSchema,
|
||||||
|
} from "./schemas/genre";
|
||||||
|
import { RequiredFrom } from "@/shared/utils/types";
|
||||||
|
|
||||||
@staticImplements<IItemService>()
|
@staticImplements<IItemService>()
|
||||||
export abstract class GameService {
|
export abstract class GameService {
|
||||||
@@ -26,7 +32,7 @@ export abstract class GameService {
|
|||||||
|
|
||||||
public static async GetCards() {
|
public static async GetCards() {
|
||||||
return await HTTPService.get(
|
return await HTTPService.get(
|
||||||
`/${this.urlPrefix}/cards`,
|
`/${this.urlPrefix}`,
|
||||||
gameCardsSchema,
|
gameCardsSchema,
|
||||||
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||||
);
|
);
|
||||||
@@ -49,7 +55,7 @@ export abstract class GameService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetEmpty(): GameCreateType {
|
public static GetEmpty(): RequiredFrom<GameCreateType> {
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
torrent_file: "",
|
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> = [
|
static propertiesDescription: ItemPropertiesDescriptionType<GameType> = [
|
||||||
[
|
[
|
||||||
{ name: "Система", key: "system" },
|
{ name: "Система", key: "system" },
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { gameCardBaseSchema } from "./gameCard";
|
import { gameCardBaseSchema } from "./gameCard";
|
||||||
|
import { gameGenresSchema } from "./genre";
|
||||||
|
import { ownerSchema } from "../../schemas/owner";
|
||||||
|
import { TypesOfItems } from "../../types";
|
||||||
|
|
||||||
export const gameBaseSchema = gameCardBaseSchema.merge(
|
export const gameBaseSchema = gameCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -27,6 +30,8 @@ export const gameBaseSchema = gameCardBaseSchema.merge(
|
|||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
),
|
),
|
||||||
|
|
||||||
|
genres: gameGenresSchema.optional().nullable(),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -36,15 +41,11 @@ export type GameCreateType = z.infer<typeof gameCreateSchema>;
|
|||||||
export const gameSchema = gameBaseSchema.merge(
|
export const gameSchema = gameBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().positive(),
|
id: z.number().positive(),
|
||||||
owner_id: z.number().positive(),
|
owner: ownerSchema,
|
||||||
update_date: z
|
update_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
upload_date: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.transform((d) => new Date(d)),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
export type GameType = z.infer<typeof gameSchema>;
|
export type GameType = z.infer<typeof gameSchema>;
|
||||||
@@ -61,3 +62,7 @@ export const gamesSchema = z.array(z.any()).transform((a) => {
|
|||||||
});
|
});
|
||||||
return games;
|
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;
|
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 {
|
import { isGame } from "./game/schemas/game";
|
||||||
gameSchema,
|
export { isGame };
|
||||||
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 { MovieService } from "./movie/movie";
|
import { MovieService } from "./movie/movie";
|
||||||
export { MovieService };
|
export { MovieService };
|
||||||
|
|
||||||
import {
|
import { isMovie } from "./movie/schemas/movie";
|
||||||
audiobookSchema,
|
export { isMovie };
|
||||||
audiobooksSchema,
|
|
||||||
audiobookCreateSchema,
|
|
||||||
type AudiobookType,
|
|
||||||
type AudiobookCreateType,
|
|
||||||
} from "./audiobook/schemas/audiobook";
|
|
||||||
export {
|
|
||||||
audiobookSchema,
|
|
||||||
audiobooksSchema,
|
|
||||||
audiobookCreateSchema,
|
|
||||||
type AudiobookType,
|
|
||||||
type AudiobookCreateType,
|
|
||||||
};
|
|
||||||
|
|
||||||
import {
|
import { isAudiobook } from "./audiobook/schemas/audiobook";
|
||||||
audiobookCardSchema,
|
export { isAudiobook };
|
||||||
audiobookCardsSchema,
|
|
||||||
type AudiobookCardType,
|
|
||||||
isAudiobook,
|
|
||||||
} from "./audiobook/schemas/audiobookCard";
|
|
||||||
export {
|
|
||||||
audiobookCardSchema,
|
|
||||||
audiobookCardsSchema,
|
|
||||||
type AudiobookCardType,
|
|
||||||
isAudiobook,
|
|
||||||
};
|
|
||||||
|
|
||||||
import { AudiobookService } from "./audiobook/audiobook";
|
|
||||||
export { AudiobookService };
|
|
||||||
|
|
||||||
import { ItemService } from "./item";
|
import { ItemService } from "./item";
|
||||||
export { ItemService };
|
export { ItemService };
|
||||||
|
|
||||||
import {
|
import {
|
||||||
TypesOfItems,
|
TypesOfItems,
|
||||||
|
isGenre,
|
||||||
type IItemService,
|
type IItemService,
|
||||||
type ItemType,
|
type ItemType,
|
||||||
type ItemCardType,
|
type ItemCardType,
|
||||||
type ItemCreateType,
|
type ItemCreateType,
|
||||||
|
type GenreType,
|
||||||
|
type CreateGenreType,
|
||||||
|
type ItemListPropertyType,
|
||||||
} from "./types";
|
} from "./types";
|
||||||
export {
|
export {
|
||||||
TypesOfItems,
|
TypesOfItems,
|
||||||
|
isGenre,
|
||||||
type IItemService,
|
type IItemService,
|
||||||
type ItemType,
|
type ItemType,
|
||||||
type ItemCardType,
|
type ItemCardType,
|
||||||
type ItemCreateType,
|
type ItemCreateType,
|
||||||
|
type GenreType,
|
||||||
|
type CreateGenreType,
|
||||||
|
type ItemListPropertyType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,6 +10,17 @@ import {
|
|||||||
TypesOfItems,
|
TypesOfItems,
|
||||||
} from "../types";
|
} from "../types";
|
||||||
import { ItemService } from "../item";
|
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>()
|
@staticImplements<IItemService>()
|
||||||
export abstract class MovieService {
|
export abstract class MovieService {
|
||||||
@@ -26,7 +37,7 @@ export abstract class MovieService {
|
|||||||
|
|
||||||
public static async GetCards() {
|
public static async GetCards() {
|
||||||
return await HTTPService.get(
|
return await HTTPService.get(
|
||||||
`/${this.urlPrefix}/cards`,
|
`/${this.urlPrefix}`,
|
||||||
movieCardsSchema,
|
movieCardsSchema,
|
||||||
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
this.cacheOptions(`/${this.urlPrefix}/cards`)
|
||||||
);
|
);
|
||||||
@@ -49,7 +60,7 @@ export abstract class MovieService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public static GetEmpty(): MovieCreateType {
|
public static GetEmpty(): RequiredFrom<MovieCreateType> {
|
||||||
return {
|
return {
|
||||||
title: "",
|
title: "",
|
||||||
torrent_file: "",
|
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> =
|
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 { z } from "zod";
|
||||||
import { movieCardBaseSchema } from "./movieCard";
|
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(
|
export const movieBaseSchema = movieCardBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
@@ -24,6 +28,9 @@ export const movieBaseSchema = movieCardBaseSchema.merge(
|
|||||||
})
|
})
|
||||||
: undefined
|
: 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(
|
export const movieSchema = movieBaseSchema.merge(
|
||||||
z.object({
|
z.object({
|
||||||
id: z.number().positive(),
|
id: z.number().positive(),
|
||||||
owner_id: z.number().positive(),
|
owner: ownerSchema,
|
||||||
update_date: z
|
update_date: z
|
||||||
.string()
|
.string()
|
||||||
.min(1)
|
.min(1)
|
||||||
.transform((d) => new Date(d)),
|
.transform((d) => new Date(d)),
|
||||||
upload_date: z
|
|
||||||
.string()
|
|
||||||
.min(1)
|
|
||||||
.transform((d) => new Date(d)),
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
export type MovieType = z.infer<typeof movieSchema>;
|
export type MovieType = z.infer<typeof movieSchema>;
|
||||||
@@ -58,3 +61,7 @@ export const moviesSchema = z.array(z.any()).transform((a) => {
|
|||||||
});
|
});
|
||||||
return games;
|
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;
|
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,
|
AudiobookType,
|
||||||
} from "./audiobook/schemas/audiobook";
|
} from "./audiobook/schemas/audiobook";
|
||||||
import { AudiobookCardType } from "./audiobook/schemas/audiobookCard";
|
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 ItemType = GameType | MovieType | AudiobookType;
|
||||||
export type ItemCardType = GameCardType | MovieCardType | AudiobookCardType;
|
export type ItemCardType = GameCardType | MovieCardType | AudiobookCardType;
|
||||||
@@ -14,9 +21,20 @@ export type ItemCreateType =
|
|||||||
| GameCreateType
|
| GameCreateType
|
||||||
| MovieCreateType
|
| MovieCreateType
|
||||||
| AudiobookCreateType;
|
| AudiobookCreateType;
|
||||||
|
|
||||||
export type UnionItemType = GameType & MovieType & AudiobookType;
|
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 {
|
export enum TypesOfItems {
|
||||||
game,
|
game,
|
||||||
movie,
|
movie,
|
||||||
@@ -39,6 +57,8 @@ export interface IItemService {
|
|||||||
Change(id: number, info: ItemCreateType): Promise<ItemType | null>;
|
Change(id: number, info: ItemCreateType): Promise<ItemType | null>;
|
||||||
GetEmpty(): ItemCreateType;
|
GetEmpty(): ItemCreateType;
|
||||||
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
|
propertiesDescription: ItemPropertiesDescriptionType<UnionItemType>;
|
||||||
|
CreateGenre(info: CreateGenreType): Promise<GenreType | null>;
|
||||||
|
GetGenres(): Promise<GenreType[] | null>;
|
||||||
}
|
}
|
||||||
export const staticImplements =
|
export const staticImplements =
|
||||||
<T>() =>
|
<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 { PersonIcon } from "./personIcon";
|
||||||
import { SunIcon } from "./sunIcon";
|
import { SunIcon } from "./sunIcon";
|
||||||
import { SpinnerIcon } from "./spinnerIcon";
|
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,
|
alt,
|
||||||
className,
|
className,
|
||||||
}: {
|
}: {
|
||||||
src: string;
|
src: string | null | undefined;
|
||||||
preview?: boolean;
|
preview?: boolean;
|
||||||
width?: number;
|
width?: number;
|
||||||
height?: number;
|
height?: number;
|
||||||
@@ -16,6 +16,8 @@ export const Img = ({
|
|||||||
className: string;
|
className: string;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{src && (
|
||||||
<Image
|
<Image
|
||||||
className={className}
|
className={className}
|
||||||
src={
|
src={
|
||||||
@@ -29,5 +31,7 @@ export const Img = ({
|
|||||||
height={height}
|
height={height}
|
||||||
alt={alt ?? ""}
|
alt={alt ?? ""}
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ type RequestOptions = GetRequestOptions & {
|
|||||||
|
|
||||||
export abstract class HTTPService {
|
export abstract class HTTPService {
|
||||||
private static deepUndefinedToNull(o?: object): object | undefined {
|
private static deepUndefinedToNull(o?: object): object | undefined {
|
||||||
|
if (Array.isArray(o)) return o;
|
||||||
if (o)
|
if (o)
|
||||||
return Object.fromEntries(
|
return Object.fromEntries(
|
||||||
Object.entries(o).map(([k, v]) => {
|
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
|
<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={(e) => {
|
||||||
|
changeMenuOpen(!open);
|
||||||
|
e.stopPropagation();
|
||||||
|
}}
|
||||||
onBlur={() => changeMenuOpen(false)}
|
onBlur={() => changeMenuOpen(false)}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
import { FilesService } from "@/entities/files";
|
import { FilesService } from "@/entities/files";
|
||||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
import { ItemCreateType } from "@/entities/item";
|
||||||
import { Img } from "@/shared/ui";
|
import { Img } from "@/shared/ui";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { UseFormSetValue } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
export const ItemCover = ({
|
export const ItemCover = ({
|
||||||
cover,
|
cover,
|
||||||
editable,
|
editable,
|
||||||
setFormValue: setValue,
|
|
||||||
}: {
|
}: {
|
||||||
cover: string | null | undefined;
|
cover: string | null | undefined;
|
||||||
editable: boolean;
|
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(
|
const onCoverDrop = useCallback(
|
||||||
(acceptedFiles: File[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
const file = acceptedFiles[0];
|
const file = acceptedFiles[0];
|
||||||
@@ -48,18 +54,16 @@ export const ItemCover = ({
|
|||||||
|
|
||||||
return (
|
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">
|
<div className="lp:w-[60%] lp:px-4 lp:pl-0 pt-2 pb-4 float-left relative">
|
||||||
{cover && (
|
|
||||||
<Img
|
<Img
|
||||||
src={cover}
|
src={watch_cover ?? cover}
|
||||||
preview={false}
|
preview={false}
|
||||||
className="transition-all rounded-lg w-full object-contain"
|
className="transition-all rounded-lg w-full object-contain"
|
||||||
width={1280}
|
width={1280}
|
||||||
height={720}
|
height={720}
|
||||||
/>
|
/>
|
||||||
)}
|
{!watch_cover && !cover && editable && (
|
||||||
{!cover && editable && (
|
|
||||||
<div className="w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
<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 { 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 clsx from "clsx";
|
||||||
import { SpinnerIcon } from "@/shared/assets/icons";
|
import { SpinnerIcon } from "@/shared/assets/icons";
|
||||||
|
|
||||||
@@ -8,23 +12,22 @@ export const ItemDetails = ({
|
|||||||
description,
|
description,
|
||||||
editable,
|
editable,
|
||||||
state,
|
state,
|
||||||
registerFormField: register,
|
|
||||||
setFormValue: setValue,
|
|
||||||
}: {
|
}: {
|
||||||
title: {
|
|
||||||
title: string;
|
title: string;
|
||||||
default_title: string;
|
|
||||||
error: string | undefined;
|
|
||||||
};
|
|
||||||
description: {
|
|
||||||
description: string | null | undefined;
|
description: string | null | undefined;
|
||||||
default_description: string | null | undefined;
|
|
||||||
};
|
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
state: "saved" | "editing" | "error";
|
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 (
|
return (
|
||||||
<span>
|
<span>
|
||||||
<span className="flex items-end justify-between relative pt-2">
|
<span className="flex items-end justify-between relative pt-2">
|
||||||
@@ -32,7 +35,7 @@ export const ItemDetails = ({
|
|||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"text-fg4 text-2xl absolute -z-10 opacity-0",
|
"text-fg4 text-2xl absolute -z-10 opacity-0",
|
||||||
title.title === "" && "opacity-100",
|
watched_title === "" && "opacity-100",
|
||||||
"transition-opacity cursor-text"
|
"transition-opacity cursor-text"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@@ -46,7 +49,7 @@ export const ItemDetails = ({
|
|||||||
)}
|
)}
|
||||||
suppressContentEditableWarning={true}
|
suppressContentEditableWarning={true}
|
||||||
contentEditable={editable}
|
contentEditable={editable}
|
||||||
{...register("title", { value: title.default_title })}
|
{...register("title", { value: title })}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setValue("title", e.currentTarget.innerText, {
|
setValue("title", e.currentTarget.innerText, {
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
@@ -54,7 +57,7 @@ export const ItemDetails = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{title.default_title}
|
{title}
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
{editable && (
|
{editable && (
|
||||||
@@ -73,14 +76,14 @@ export const ItemDetails = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
<div className="text-err text-xs w-full h-2">{title.error}</div>
|
<div className="text-err text-xs w-full h-2">{errors.title?.message}</div>
|
||||||
{(description.default_description || editable) && (
|
{(description || editable) && (
|
||||||
<span className="relative">
|
<span className="relative">
|
||||||
{editable && (
|
{editable && (
|
||||||
<span
|
<span
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"text-fg4 text-md absolute -z-10 opacity-0",
|
"text-fg4 text-md absolute -z-10 opacity-0",
|
||||||
(description.description === "" || description === undefined) &&
|
(watched_description === "" || description === undefined) &&
|
||||||
"opacity-100",
|
"opacity-100",
|
||||||
"transition-opacity mt-2"
|
"transition-opacity mt-2"
|
||||||
)}
|
)}
|
||||||
@@ -97,7 +100,7 @@ export const ItemDetails = ({
|
|||||||
!editable && "cursor-default"
|
!editable && "cursor-default"
|
||||||
)}
|
)}
|
||||||
{...register("description", {
|
{...register("description", {
|
||||||
value: description.default_description,
|
value: description,
|
||||||
})}
|
})}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setValue("description", e.currentTarget.innerText, {
|
setValue("description", e.currentTarget.innerText, {
|
||||||
@@ -106,7 +109,7 @@ export const ItemDetails = ({
|
|||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{description.default_description}
|
{description}
|
||||||
</div>
|
</div>
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,21 +1,25 @@
|
|||||||
import { FilesService } from "@/entities/files";
|
import { FilesService } from "@/entities/files";
|
||||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
import { ItemCreateType } from "@/entities/item";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const ItemFragment = ({
|
export const ItemFragment = ({
|
||||||
fragment,
|
fragment,
|
||||||
editable,
|
editable,
|
||||||
setFormValue: setValue,
|
|
||||||
registerFormField: register,
|
|
||||||
}: {
|
}: {
|
||||||
fragment: string | undefined | null;
|
fragment: string | undefined | null;
|
||||||
editable: boolean;
|
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(
|
const onFragmentDrop = useCallback(
|
||||||
(acceptedFiles: File[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
const file = acceptedFiles[0];
|
const file = acceptedFiles[0];
|
||||||
@@ -57,8 +61,15 @@ export const ItemFragment = ({
|
|||||||
controls
|
controls
|
||||||
controlsList="nodownload"
|
controlsList="nodownload"
|
||||||
typeof="audio/mpeg"
|
typeof="audio/mpeg"
|
||||||
src={process.env.NEXT_PUBLIC_FRAGMENT_URL + "/" + (fragment ?? "")}
|
src={
|
||||||
className={clsx(!fragment && "pointer-events-none", "w-full h-full")}
|
process.env.NEXT_PUBLIC_FRAGMENT_URL +
|
||||||
|
"/" +
|
||||||
|
(watched_fragment ?? "")
|
||||||
|
}
|
||||||
|
className={clsx(
|
||||||
|
!watched_fragment && "pointer-events-none",
|
||||||
|
"w-full h-full"
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
{editable && (
|
{editable && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -6,22 +6,24 @@ import {
|
|||||||
isMovie,
|
isMovie,
|
||||||
ItemCreateType,
|
ItemCreateType,
|
||||||
ItemType,
|
ItemType,
|
||||||
|
MovieService,
|
||||||
} from "@/entities/item";
|
} from "@/entities/item";
|
||||||
import { UserService } from "@/entities/user";
|
import { UserService } from "@/entities/user";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
import { useEffect, useRef, useState } from "react";
|
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 { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { ItemCover } from "./itemCover";
|
import { ItemCover } from "./itemCover";
|
||||||
import { ItemService } from "@/entities/item/item";
|
import { ItemService as IS } from "@/entities/item/item";
|
||||||
import { ItemProperties } from "./itemProperties";
|
import { ItemProperties } from "./itemProperties";
|
||||||
import { ItemTrailer } from "./itemTrailer";
|
import { ItemTrailer } from "./itemTrailer";
|
||||||
import { ItemTorrent } from "./itemTorrent";
|
import { ItemTorrent } from "./itemTorrent";
|
||||||
import { ItemDetails } from "./itemDetails";
|
import { ItemDetails } from "./itemDetails";
|
||||||
import { ItemFragment } from "./itemFragment";
|
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: init_item,
|
||||||
}: {
|
}: {
|
||||||
item: T;
|
item: T;
|
||||||
@@ -33,140 +35,118 @@ export const ItemInfo = <T extends ItemType | ItemCreateType>({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (me) {
|
if (me) {
|
||||||
if (ItemService.isExistingItem(item))
|
if (IS.isExistingItem(item)) setEditable(me.id === item.owner.id);
|
||||||
setEditable(me.id === item.owner_id);
|
|
||||||
else setEditable(true);
|
else setEditable(true);
|
||||||
}
|
}
|
||||||
}, [me, item]);
|
}, [me, item]);
|
||||||
|
|
||||||
const formRef = useRef<HTMLFormElement>(null);
|
const formRef = useRef<HTMLFormElement>(null);
|
||||||
const {
|
const form = useForm<ItemCreateType>({
|
||||||
register,
|
|
||||||
handleSubmit,
|
|
||||||
setValue,
|
|
||||||
setError,
|
|
||||||
watch,
|
|
||||||
reset,
|
|
||||||
formState: { dirtyFields, errors },
|
|
||||||
} = useForm<ItemType | ItemCreateType>({
|
|
||||||
defaultValues: init_item,
|
defaultValues: init_item,
|
||||||
resolver: zodResolver(
|
resolver: zodResolver(IS.itemsConfiguration[item.type].formResolver),
|
||||||
ItemService.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>(
|
const [savedTimeout, changeSavedTimeout] = useState<NodeJS.Timeout | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
const watchedData = watch();
|
const watch = form.watch();
|
||||||
|
|
||||||
const [formData, changeFormData] = useState<T | null>(null);
|
const [formData, changeFormData] = useState<T | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!Object.keys(dirtyFields).length) return;
|
if (!Object.keys(form.formState.dirtyFields).length) return;
|
||||||
if (JSON.stringify(watchedData) === JSON.stringify(formData)) return;
|
if (JSON.stringify(watch) === JSON.stringify(formData)) return;
|
||||||
changeFormData(watchedData as T);
|
changeFormData(watch as T);
|
||||||
if (savedTimeout) clearTimeout(savedTimeout);
|
if (savedTimeout) clearTimeout(savedTimeout);
|
||||||
changeSavedTimeout(
|
changeSavedTimeout(
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
if (formRef.current) formRef.current.requestSubmit();
|
if (formRef.current) formRef.current.requestSubmit();
|
||||||
}, 3000)
|
}, 3000)
|
||||||
);
|
);
|
||||||
}, [watchedData]);
|
}, [watch]);
|
||||||
|
|
||||||
const onSubmit = async (formData: ItemCreateType) => {
|
const onSubmit = async (formData: ItemCreateType) => {
|
||||||
const updatedItem = ItemService.isExistingItem(item)
|
const updatedItem = IS.isExistingItem(item)
|
||||||
? await ItemService.ChangeItem(item.id, formData)
|
? await IS.ChangeItem(item.id, formData)
|
||||||
: await ItemService.AddItem(formData);
|
: await IS.AddItem(formData);
|
||||||
changeSavedTimeout(null);
|
changeSavedTimeout(null);
|
||||||
if (updatedItem) {
|
if (updatedItem) {
|
||||||
changeItem(updatedItem as T);
|
changeItem(updatedItem as T);
|
||||||
reset({}, { keepValues: true });
|
form.reset({}, { keepValues: true });
|
||||||
} else {
|
} else {
|
||||||
setError("root", { message: "Ошибка сервера" });
|
form.setError("root", { message: "Ошибка сервера" });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => console.log(errors), [errors]);
|
useEffect(() => console.log(form.formState.errors), [form.formState.errors]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<FormProvider {...form}>
|
||||||
<form
|
<form
|
||||||
onSubmit={handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
className="m-4 flex flex-col lp:block"
|
className="m-4 flex flex-col lp:block"
|
||||||
ref={formRef}
|
ref={formRef}
|
||||||
>
|
>
|
||||||
<ItemCover
|
<ItemCover
|
||||||
cover={watchedData.cover}
|
cover={IS.isExistingItem(item) ? item.cover : null}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
setFormValue={setValue}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ItemDetails
|
<ItemDetails
|
||||||
title={{
|
title={item.title}
|
||||||
title: watchedData.title,
|
description={IS.isExistingItem(item) ? item.description : null}
|
||||||
default_title: item.title,
|
|
||||||
error: errors.title?.message,
|
|
||||||
}}
|
|
||||||
description={{
|
|
||||||
description: watchedData.description,
|
|
||||||
default_description: item.description,
|
|
||||||
}}
|
|
||||||
editable={editable}
|
editable={editable}
|
||||||
state={
|
state={
|
||||||
savedTimeout
|
savedTimeout
|
||||||
? Object.keys(errors).length > 0
|
? Object.keys(form.formState.errors).length > 0
|
||||||
? "error"
|
? "error"
|
||||||
: "editing"
|
: "editing"
|
||||||
: "saved"
|
: "saved"
|
||||||
}
|
}
|
||||||
registerFormField={register}
|
|
||||||
setFormValue={setValue}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<ItemProperties
|
<ItemListProperties
|
||||||
item={item}
|
propertyName="genres"
|
||||||
watchedFormData={watchedData}
|
propertyList={IS.isExistingItem(item) ? item.genres : null}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
setFormValue={setValue}
|
getAllTags={async () =>
|
||||||
registerFormField={register}
|
await IS.itemsConfiguration[item.type].service.GetGenres()
|
||||||
|
}
|
||||||
|
createTag={async (property: string) =>
|
||||||
|
await IS.itemsConfiguration[item.type].service.CreateGenre({
|
||||||
|
genre: property,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{(isGame(item) || isMovie(item)) &&
|
<ItemProperties item={item} editable={editable} />
|
||||||
(isGame(watchedData) || isMovie(watchedData)) && (
|
|
||||||
<ItemTrailer
|
{isMovie(item) && (
|
||||||
default_trailer={item.trailer}
|
<ItemListProperties
|
||||||
trailer={watchedData.trailer}
|
propertyName="actors"
|
||||||
|
propertyList={IS.isExistingItem(item) ? item.actors : null}
|
||||||
editable={editable}
|
editable={editable}
|
||||||
registerFormField={register}
|
getAllTags={async () => await MovieService.GetActors()}
|
||||||
setFormValue={setValue}
|
createTag={async (property: string) =>
|
||||||
|
await MovieService.CreateActor({ actor: property })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isAudiobook(watchedData) && (
|
{((isGame(item) && isGame(watch)) ||
|
||||||
<ItemFragment
|
(isMovie(item) && isMovie(watch))) && (
|
||||||
fragment={watchedData.fragment}
|
<ItemTrailer default_trailer={item.trailer} editable={editable} />
|
||||||
editable={editable}
|
|
||||||
registerFormField={register}
|
|
||||||
setFormValue={setValue}
|
|
||||||
/>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ItemTorrent
|
{isAudiobook(item) && isAudiobook(watch) && (
|
||||||
title={watchedData.title}
|
<ItemFragment fragment={item.fragment} editable={editable} />
|
||||||
torrent_file={watchedData.torrent_file}
|
)}
|
||||||
editable={editable}
|
|
||||||
error={errors.torrent_file?.message}
|
<ItemTorrent torrent_file={item.torrent_file} editable={editable} />
|
||||||
setFormValue={setValue}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<input type="submit" className="hidden" />
|
<input type="submit" className="hidden" />
|
||||||
</form>
|
</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 { ItemCreateType, ItemType } from "@/entities/item";
|
||||||
import { ItemService } from "@/entities/item/item";
|
import { ItemService } from "@/entities/item/item";
|
||||||
import { ItemPropertiesDescriptionType } from "@/entities/item/types";
|
import { ItemPropertiesDescriptionType } from "@/entities/item/types";
|
||||||
|
import { RequiredFrom } from "@/shared/utils/types";
|
||||||
import clsx from "clsx";
|
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,
|
item,
|
||||||
watchedFormData: watchedData,
|
|
||||||
editable,
|
editable,
|
||||||
setFormValue: setValue,
|
|
||||||
registerFormField: register,
|
|
||||||
}: {
|
}: {
|
||||||
item: T; // Init values
|
item: T; // Init values
|
||||||
watchedFormData: T; // Updated values
|
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
|
||||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { register, setValue, watch } = useFormContext<ItemCreateType>();
|
||||||
|
|
||||||
|
const watchedData = watch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={clsx(
|
className={clsx(
|
||||||
@@ -26,7 +31,7 @@ export const ItemProperties = <T extends ItemType | ItemCreateType>({
|
|||||||
>
|
>
|
||||||
{(
|
{(
|
||||||
ItemService.itemsConfiguration[item.type]
|
ItemService.itemsConfiguration[item.type]
|
||||||
.propertiesDescription as ItemPropertiesDescriptionType<T>
|
.propertiesDescription as ItemPropertiesDescriptionType<ItemCreateType>
|
||||||
).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) => (
|
||||||
@@ -54,27 +59,25 @@ export const ItemProperties = <T extends ItemType | ItemCreateType>({
|
|||||||
(watchedData[req.key] as string) === "") &&
|
(watchedData[req.key] as string) === "") &&
|
||||||
"opacity-100 absolute left-0 top-0 inline-block min-w-10 z-10"
|
"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
|
value: req.value
|
||||||
? req.value(item)
|
? req.value(item as ItemCreateType)
|
||||||
: undefined ?? (item[req.key] as string),
|
: undefined ??
|
||||||
|
((item as ItemCreateType)[req.key] as string),
|
||||||
})}
|
})}
|
||||||
contentEditable={editable && (req.editable ?? true)}
|
contentEditable={editable && (req.editable ?? true)}
|
||||||
suppressContentEditableWarning={true}
|
suppressContentEditableWarning={true}
|
||||||
onInput={(e) => {
|
onInput={(e) => {
|
||||||
setValue(
|
setValue(req.key, e.currentTarget.innerText, {
|
||||||
req.key as keyof ItemType,
|
|
||||||
e.currentTarget.innerText,
|
|
||||||
{
|
|
||||||
shouldValidate: true,
|
shouldValidate: true,
|
||||||
shouldDirty: true,
|
shouldDirty: true,
|
||||||
}
|
});
|
||||||
);
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{req.value
|
{req.value
|
||||||
? req.value(item)
|
? req.value(item as ItemCreateType)
|
||||||
: undefined ?? (item[req.key] as string)}
|
: undefined ??
|
||||||
|
((item as ItemCreateType)[req.key] as string)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
|
|||||||
@@ -1,24 +1,32 @@
|
|||||||
import { FilesService } from "@/entities/files";
|
import { FilesService } from "@/entities/files";
|
||||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
import { ItemCreateType, ItemType } from "@/entities/item";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useCallback } from "react";
|
import { useCallback, useEffect } from "react";
|
||||||
import { useDropzone } from "react-dropzone";
|
import { useDropzone } from "react-dropzone";
|
||||||
import { UseFormSetValue } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
export const ItemTorrent = ({
|
export const ItemTorrent = ({
|
||||||
title,
|
|
||||||
torrent_file,
|
torrent_file,
|
||||||
editable,
|
editable,
|
||||||
error,
|
|
||||||
setFormValue: setValue,
|
|
||||||
}: {
|
}: {
|
||||||
title: string;
|
|
||||||
torrent_file: string;
|
torrent_file: string;
|
||||||
editable: boolean;
|
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(
|
const onTorrentDrop = useCallback(
|
||||||
(acceptedFiles: File[]) => {
|
(acceptedFiles: File[]) => {
|
||||||
const file = acceptedFiles[0];
|
const file = acceptedFiles[0];
|
||||||
@@ -58,21 +66,23 @@ export const ItemTorrent = ({
|
|||||||
>
|
>
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<Link
|
<Link
|
||||||
href={process.env.NEXT_PUBLIC_CONTENT_URL + "/" + torrent_file}
|
href={
|
||||||
|
process.env.NEXT_PUBLIC_CONTENT_URL + "/" + watched_torrent_file
|
||||||
|
}
|
||||||
className={clsx(
|
className={clsx(
|
||||||
"p-4 bg-ac0 text-fg1 text-2xl rounded-lg",
|
"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>
|
</Link>
|
||||||
{editable && (
|
{editable && (
|
||||||
<>
|
<>
|
||||||
<input {...getTorrentDropInputProps()} />
|
<input {...getTorrentDropInputProps()} />
|
||||||
<span className="flex flex-col items-center w-full p-1 text-fg4 text-xs lp:text-sm cursor-pointer">
|
<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">
|
||||||
<span className="w-full text-center text-err">{error}</span>
|
{errors.torrent_file?.message}
|
||||||
)}
|
</span>
|
||||||
{isTorrentDragActive ? (
|
{isTorrentDragActive ? (
|
||||||
<span className="w-full text-center">
|
<span className="w-full text-center">
|
||||||
Изменить .torrent файл...
|
Изменить .torrent файл...
|
||||||
|
|||||||
@@ -1,32 +1,30 @@
|
|||||||
import { ItemCreateType, ItemType } from "@/entities/item";
|
import { ItemCreateType } from "@/entities/item";
|
||||||
import { getYouTubeID } from "@/shared/utils/getYoutubeId";
|
import { getYouTubeID } from "@/shared/utils/getYoutubeId";
|
||||||
import { UseFormRegister, UseFormSetValue } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
export const ItemTrailer = ({
|
export const ItemTrailer = ({
|
||||||
trailer,
|
|
||||||
default_trailer,
|
default_trailer,
|
||||||
editable,
|
editable,
|
||||||
setFormValue: setValue,
|
|
||||||
registerFormField: register,
|
|
||||||
}: {
|
}: {
|
||||||
trailer: string | undefined | null;
|
|
||||||
default_trailer: string | undefined | null;
|
default_trailer: string | undefined | null;
|
||||||
editable: boolean;
|
editable: boolean;
|
||||||
setFormValue: UseFormSetValue<ItemType | ItemCreateType>;
|
|
||||||
registerFormField: UseFormRegister<ItemType | ItemCreateType>;
|
|
||||||
}) => {
|
}) => {
|
||||||
|
const { register, setValue, watch } = useFormContext<ItemCreateType>();
|
||||||
|
|
||||||
|
const watched_trailer = watch("trailer");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{(trailer || editable) && (
|
{(watched_trailer || editable) && (
|
||||||
<div className="w-ful aspect-video">
|
<div className="w-ful aspect-video">
|
||||||
{trailer && getYouTubeID(trailer) && (
|
{watched_trailer && getYouTubeID(watched_trailer) && (
|
||||||
<iframe
|
<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"
|
className="w-full aspect-video rounded-lg mt-4"
|
||||||
allowFullScreen
|
allowFullScreen
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!trailer && editable && (
|
{!watched_trailer && editable && (
|
||||||
<div className="mt-4 w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
<div className="mt-4 w-full aspect-video border-dashed border-2 border-bg1 rounded-lg"></div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -42,7 +40,7 @@ export const ItemTrailer = ({
|
|||||||
setValue("trailer", e.target.value);
|
setValue("trailer", e.target.value);
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
defaultValue={default_trailer}
|
defaultValue={default_trailer ?? ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user