diff --git a/modules/frontend/src/api/services/DefaultService.ts b/modules/frontend/src/api/services/DefaultService.ts index 5070fae..218b461 100644 --- a/modules/frontend/src/api/services/DefaultService.ts +++ b/modules/frontend/src/api/services/DefaultService.ts @@ -199,7 +199,7 @@ export class DefaultService { * @returns any List of user titles * @throws ApiError */ - public static getUsersTitles( + public static getUserTitles( userId: string, cursor?: string, sort?: TitleSort, @@ -278,27 +278,54 @@ export class DefaultService { }, }); } + /** + * Get user title + * @param userId + * @param titleId + * @returns UserTitleMini User titles + * @throws ApiError + */ + public static getUserTitle( + userId: number, + titleId: number, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'GET', + url: '/users/{user_id}/titles/{title_id}', + path: { + 'user_id': userId, + 'title_id': titleId, + }, + errors: { + 400: `Request params are not correct`, + 404: `User or title not found`, + 500: `Unknown server error`, + }, + }); + } /** * Update a usertitle * User updating title list of watched - * @param userId ID of the user to assign the title to + * @param userId + * @param titleId * @param requestBody * @returns UserTitleMini Title successfully updated * @throws ApiError */ public static updateUserTitle( userId: number, + titleId: number, requestBody: { - title_id: number; status?: UserTitleStatus; rate?: number; }, ): CancelablePromise { return __request(OpenAPI, { method: 'PATCH', - url: '/users/{user_id}/titles', + url: '/users/{user_id}/titles/{title_id}', path: { 'user_id': userId, + 'title_id': titleId, }, body: requestBody, mediaType: 'application/json', @@ -311,4 +338,31 @@ export class DefaultService { }, }); } + /** + * Delete a usertitle + * User deleting title from list of watched + * @param userId + * @param titleId + * @returns any Title successfully deleted + * @throws ApiError + */ + public static deleteUserTitle( + userId: number, + titleId: number, + ): CancelablePromise { + return __request(OpenAPI, { + method: 'DELETE', + url: '/users/{user_id}/titles/{title_id}', + path: { + 'user_id': userId, + 'title_id': titleId, + }, + errors: { + 401: `Unauthorized — missing or invalid auth token`, + 403: `Forbidden — user not allowed to delete title`, + 404: `User or Title not found`, + 500: `Internal server error`, + }, + }); + } } diff --git a/modules/frontend/src/auth/core/OpenAPI.ts b/modules/frontend/src/auth/core/OpenAPI.ts index 79aa305..2d0edf8 100644 --- a/modules/frontend/src/auth/core/OpenAPI.ts +++ b/modules/frontend/src/auth/core/OpenAPI.ts @@ -20,7 +20,7 @@ export type OpenAPIConfig = { }; export const OpenAPI: OpenAPIConfig = { - BASE: 'http://10.1.0.65:8081/auth', + BASE: '/auth', VERSION: '1.0.0', WITH_CREDENTIALS: false, CREDENTIALS: 'include', diff --git a/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx new file mode 100644 index 0000000..0c9c741 --- /dev/null +++ b/modules/frontend/src/components/TitleStatusControls/TitleStatusControls.tsx @@ -0,0 +1,88 @@ +import { useEffect, useState } from "react"; +import { DefaultService } from "../../api"; +import type { UserTitleStatus } from "../../api"; +import { + ClockIcon, + CheckCircleIcon, + PlayCircleIcon, + XCircleIcon, +} from "@heroicons/react/24/solid"; + +// Статусы с иконками и подписью +const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ + { status: "planned", icon: , label: "Planned" }, + { status: "finished", icon: , label: "Finished" }, + { status: "in-progress", icon: , label: "In Progress" }, + { status: "dropped", icon: , label: "Dropped" }, +]; + +export function TitleStatusControls({ titleId }: { titleId: number }) { + const [currentStatus, setCurrentStatus] = useState(null); + const [loading, setLoading] = useState(false); + + const userIdStr = localStorage.getItem("userId"); + const userId = userIdStr ? Number(userIdStr) : null; + + // --- Load initial status --- + useEffect(() => { + if (!userId) return; + + DefaultService.getUserTitle(userId, titleId) + .then((res) => setCurrentStatus(res.status)) + .catch(() => setCurrentStatus(null)); // 404 = user title does not exist + }, [titleId, userId]); + + // --- Handle click --- + const handleStatusClick = async (status: UserTitleStatus) => { + if (!userId || loading) return; + + setLoading(true); + + try { + // 1) Если кликнули на текущий статус — DELETE + if (currentStatus === status) { + await DefaultService.deleteUserTitle(userId, titleId); + setCurrentStatus(null); + return; + } + + // 2) Если другой статус — POST или PATCH + if (!currentStatus) { + // ещё нет записи — POST + const added = await DefaultService.addUserTitle(userId, { + title_id: titleId, + status, + }); + setCurrentStatus(added.status); + } else { + // уже есть запись — PATCH + const updated = await DefaultService.updateUserTitle(userId, titleId, { status }); + setCurrentStatus(updated.status); + } + } finally { + setLoading(false); + } + }; + + return ( +
+ {STATUS_BUTTONS.map(btn => ( + + ))} +
+ ); +} diff --git a/modules/frontend/src/pages/TitlePage/TitlePage.tsx b/modules/frontend/src/pages/TitlePage/TitlePage.tsx index 5ea0e3d..01f9c49 100644 --- a/modules/frontend/src/pages/TitlePage/TitlePage.tsx +++ b/modules/frontend/src/pages/TitlePage/TitlePage.tsx @@ -1,20 +1,8 @@ import { useEffect, useState } from "react"; -import { useParams } from "react-router-dom"; +import { useParams, Link } from "react-router-dom"; import { DefaultService } from "../../api/services/DefaultService"; -import type { Title, UserTitleStatus } from "../../api"; -import { - ClockIcon, - CheckCircleIcon, - PlayCircleIcon, - XCircleIcon, -} from "@heroicons/react/24/solid"; - -const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [ - { status: "planned", icon: , label: "Planned" }, - { status: "finished", icon: , label: "Finished" }, - { status: "in-progress", icon: , label: "In Progress" }, - { status: "dropped", icon: , label: "Dropped" }, -]; +import type { Title } from "../../api"; +import { TitleStatusControls } from "../../components/TitleStatusControls/TitleStatusControls"; export default function TitlePage() { const params = useParams(); @@ -24,9 +12,9 @@ export default function TitlePage() { const [loading, setLoading] = useState(true); const [error, setError] = useState(null); - const [userStatus, setUserStatus] = useState(null); - const [updatingStatus, setUpdatingStatus] = useState(false); - + // --------------------------- + // LOAD TITLE INFO + // --------------------------- useEffect(() => { const fetchTitle = async () => { setLoading(true); @@ -44,30 +32,6 @@ export default function TitlePage() { fetchTitle(); }, [titleId]); - const handleStatusClick = async (status: UserTitleStatus) => { - if (updatingStatus || userStatus === status) return; - - const userId = Number(localStorage.getItem("userId")); - if (!userId) { - alert("You must be logged in to set status."); - return; - } - - setUpdatingStatus(true); - try { - await DefaultService.addUserTitle(userId, { - title_id: titleId, - status, - }); - setUserStatus(status); - } catch (err: any) { - console.error(err); - alert(err?.message || "Failed to set status"); - } finally { - setUpdatingStatus(false); - } - }; - const getTagsString = () => title?.tags?.map(tag => tag.en).filter(Boolean).join(", "); @@ -78,7 +42,7 @@ export default function TitlePage() { return (
- {/* Постер */} + {/* Poster + status buttons */}
- {/* Статус кнопки с иконками */} -
- {STATUS_BUTTONS.map(btn => ( - - ))} -
+ {/* Status buttons */} +
- {/* Информация о тайтле */} + {/* Title info */}

{title.title_names?.en?.[0] || "Untitled"}

- {title.studio &&

Studio: {title.studio.name}

} + + {title.studio && ( +

+ Studio:{" "} + {title.studio.id ? ( + + {title.studio.name} + + ) : ( + title.studio.name + )} +

+ )} + {title.title_status &&

Status: {title.title_status}

} + {title.rating !== undefined && (

Rating: {title.rating} ({title.rating_count} votes)

)} + {title.release_year && (

Released: {title.release_year} {title.release_season || ""}

)} + {title.episodes_aired !== undefined && (

Episodes: {title.episodes_aired}/{title.episodes_all}

)} + {title.tags && title.tags.length > 0 && (

Tags: {getTagsString()} diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx index 494ba99..7cc0db5 100644 --- a/modules/frontend/src/pages/UserPage/UserPage.tsx +++ b/modules/frontend/src/pages/UserPage/UserPage.tsx @@ -63,7 +63,7 @@ export default function UserPage({ userId }: UserPageProps) { : ""; try { - const result = await DefaultService.getUsersTitles( + const result = await DefaultService.getUserTitles( id, cursorStr, sort,