Add genres and actors

This commit is contained in:
2024-07-06 20:09:02 +04:00
parent ea723b88b0
commit 05cfd9c955
34 changed files with 671 additions and 324 deletions

View File

@@ -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

View File

@@ -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"
/> />

View File

@@ -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"
/> />

View File

@@ -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";

View File

@@ -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" },

View File

@@ -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;
};

View File

@@ -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;
};

View 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;
});

View File

@@ -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" },

View File

@@ -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;
};

View File

@@ -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;
};

View 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;
});

View File

@@ -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,
}; };

View File

@@ -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> =
[ [
[ [

View 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;
});

View 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;
});

View File

@@ -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;
};

View File

@@ -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;
};

View 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),
});

View File

@@ -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>() =>

View 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>
);
};

View File

@@ -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 };

View File

@@ -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 ?? ""}
/> />
)}
</>
); );
}; };

View File

@@ -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]) => {

View 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];
};

View File

@@ -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

View File

@@ -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>
)} )}

View File

@@ -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>
)} )}

View File

@@ -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 && (
<> <>

View File

@@ -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>
); );
}; };

View 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>
);
};

View File

@@ -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>

View File

@@ -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 файл...

View File

@@ -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>
)} )}