Merge branch 'front' into dev
This commit is contained in:
commit
f843c23e57
5 changed files with 179 additions and 69 deletions
|
|
@ -199,7 +199,7 @@ export class DefaultService {
|
||||||
* @returns any List of user titles
|
* @returns any List of user titles
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static getUsersTitles(
|
public static getUserTitles(
|
||||||
userId: string,
|
userId: string,
|
||||||
cursor?: string,
|
cursor?: string,
|
||||||
sort?: TitleSort,
|
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<UserTitleMini> {
|
||||||
|
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
|
* Update a usertitle
|
||||||
* User updating title list of watched
|
* User updating title list of watched
|
||||||
* @param userId ID of the user to assign the title to
|
* @param userId
|
||||||
|
* @param titleId
|
||||||
* @param requestBody
|
* @param requestBody
|
||||||
* @returns UserTitleMini Title successfully updated
|
* @returns UserTitleMini Title successfully updated
|
||||||
* @throws ApiError
|
* @throws ApiError
|
||||||
*/
|
*/
|
||||||
public static updateUserTitle(
|
public static updateUserTitle(
|
||||||
userId: number,
|
userId: number,
|
||||||
|
titleId: number,
|
||||||
requestBody: {
|
requestBody: {
|
||||||
title_id: number;
|
|
||||||
status?: UserTitleStatus;
|
status?: UserTitleStatus;
|
||||||
rate?: number;
|
rate?: number;
|
||||||
},
|
},
|
||||||
): CancelablePromise<UserTitleMini> {
|
): CancelablePromise<UserTitleMini> {
|
||||||
return __request(OpenAPI, {
|
return __request(OpenAPI, {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
url: '/users/{user_id}/titles',
|
url: '/users/{user_id}/titles/{title_id}',
|
||||||
path: {
|
path: {
|
||||||
'user_id': userId,
|
'user_id': userId,
|
||||||
|
'title_id': titleId,
|
||||||
},
|
},
|
||||||
body: requestBody,
|
body: requestBody,
|
||||||
mediaType: 'application/json',
|
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<any> {
|
||||||
|
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`,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export type OpenAPIConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OpenAPI: OpenAPIConfig = {
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
BASE: 'http://10.1.0.65:8081/auth',
|
BASE: '/auth',
|
||||||
VERSION: '1.0.0',
|
VERSION: '1.0.0',
|
||||||
WITH_CREDENTIALS: false,
|
WITH_CREDENTIALS: false,
|
||||||
CREDENTIALS: 'include',
|
CREDENTIALS: 'include',
|
||||||
|
|
|
||||||
|
|
@ -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: <ClockIcon className="w-5 h-5" />, label: "Planned" },
|
||||||
|
{ status: "finished", icon: <CheckCircleIcon className="w-5 h-5" />, label: "Finished" },
|
||||||
|
{ status: "in-progress", icon: <PlayCircleIcon className="w-5 h-5" />, label: "In Progress" },
|
||||||
|
{ status: "dropped", icon: <XCircleIcon className="w-5 h-5" />, label: "Dropped" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export function TitleStatusControls({ titleId }: { titleId: number }) {
|
||||||
|
const [currentStatus, setCurrentStatus] = useState<UserTitleStatus | null>(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 (
|
||||||
|
<div className="flex gap-2 flex-wrap justify-center mt-2">
|
||||||
|
{STATUS_BUTTONS.map(btn => (
|
||||||
|
<button
|
||||||
|
key={btn.status}
|
||||||
|
onClick={() => handleStatusClick(btn.status)}
|
||||||
|
disabled={loading}
|
||||||
|
className={`
|
||||||
|
px-3 py-1 rounded-md border flex items-center gap-1 transition
|
||||||
|
${currentStatus === btn.status
|
||||||
|
? "bg-blue-600 text-white border-blue-700"
|
||||||
|
: "bg-gray-200 text-black border-gray-300 hover:bg-gray-300"}
|
||||||
|
`}
|
||||||
|
title={btn.label}
|
||||||
|
>
|
||||||
|
{btn.icon}
|
||||||
|
<span>{btn.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -1,20 +1,8 @@
|
||||||
import { useEffect, useState } from "react";
|
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 { DefaultService } from "../../api/services/DefaultService";
|
||||||
import type { Title, UserTitleStatus } from "../../api";
|
import type { Title } from "../../api";
|
||||||
import {
|
import { TitleStatusControls } from "../../components/TitleStatusControls/TitleStatusControls";
|
||||||
ClockIcon,
|
|
||||||
CheckCircleIcon,
|
|
||||||
PlayCircleIcon,
|
|
||||||
XCircleIcon,
|
|
||||||
} from "@heroicons/react/24/solid";
|
|
||||||
|
|
||||||
const STATUS_BUTTONS: { status: UserTitleStatus; icon: React.ReactNode; label: string }[] = [
|
|
||||||
{ status: "planned", icon: <ClockIcon className="w-6 h-6" />, label: "Planned" },
|
|
||||||
{ status: "finished", icon: <CheckCircleIcon className="w-6 h-6" />, label: "Finished" },
|
|
||||||
{ status: "in-progress", icon: <PlayCircleIcon className="w-6 h-6" />, label: "In Progress" },
|
|
||||||
{ status: "dropped", icon: <XCircleIcon className="w-6 h-6" />, label: "Dropped" },
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function TitlePage() {
|
export default function TitlePage() {
|
||||||
const params = useParams();
|
const params = useParams();
|
||||||
|
|
@ -24,9 +12,9 @@ export default function TitlePage() {
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
const [userStatus, setUserStatus] = useState<UserTitleStatus | null>(null);
|
// ---------------------------
|
||||||
const [updatingStatus, setUpdatingStatus] = useState(false);
|
// LOAD TITLE INFO
|
||||||
|
// ---------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchTitle = async () => {
|
const fetchTitle = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
@ -44,30 +32,6 @@ export default function TitlePage() {
|
||||||
fetchTitle();
|
fetchTitle();
|
||||||
}, [titleId]);
|
}, [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 = () =>
|
const getTagsString = () =>
|
||||||
title?.tags?.map(tag => tag.en).filter(Boolean).join(", ");
|
title?.tags?.map(tag => tag.en).filter(Boolean).join(", ");
|
||||||
|
|
||||||
|
|
@ -78,7 +42,7 @@ export default function TitlePage() {
|
||||||
return (
|
return (
|
||||||
<div className="w-full min-h-screen bg-gray-50 p-6 flex justify-center">
|
<div className="w-full min-h-screen bg-gray-50 p-6 flex justify-center">
|
||||||
<div className="flex flex-col md:flex-row bg-white shadow-lg rounded-xl max-w-4xl w-full p-6 gap-6">
|
<div className="flex flex-col md:flex-row bg-white shadow-lg rounded-xl max-w-4xl w-full p-6 gap-6">
|
||||||
{/* Постер */}
|
{/* Poster + status buttons */}
|
||||||
<div className="flex flex-col items-center">
|
<div className="flex flex-col items-center">
|
||||||
<img
|
<img
|
||||||
src={title.poster?.image_path || "/default-poster.png"}
|
src={title.poster?.image_path || "/default-poster.png"}
|
||||||
|
|
@ -86,48 +50,52 @@ export default function TitlePage() {
|
||||||
className="w-48 h-72 object-cover rounded-lg mb-4"
|
className="w-48 h-72 object-cover rounded-lg mb-4"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* Статус кнопки с иконками */}
|
{/* Status buttons */}
|
||||||
<div className="flex gap-2 mt-2 flex-wrap justify-center">
|
<TitleStatusControls titleId={titleId} />
|
||||||
{STATUS_BUTTONS.map(btn => (
|
|
||||||
<button
|
|
||||||
key={btn.status}
|
|
||||||
onClick={() => handleStatusClick(btn.status)}
|
|
||||||
disabled={updatingStatus}
|
|
||||||
className={`p-2 rounded-lg transition flex items-center justify-center ${
|
|
||||||
userStatus === btn.status
|
|
||||||
? "bg-blue-600 text-white"
|
|
||||||
: "bg-gray-200 text-gray-700 hover:bg-gray-300"
|
|
||||||
}`}
|
|
||||||
title={btn.label}
|
|
||||||
>
|
|
||||||
{btn.icon}
|
|
||||||
</button>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Информация о тайтле */}
|
{/* Title info */}
|
||||||
<div className="flex-1 flex flex-col">
|
<div className="flex-1 flex flex-col">
|
||||||
<h1 className="text-3xl font-bold mb-2">
|
<h1 className="text-3xl font-bold mb-2">
|
||||||
{title.title_names?.en?.[0] || "Untitled"}
|
{title.title_names?.en?.[0] || "Untitled"}
|
||||||
</h1>
|
</h1>
|
||||||
{title.studio && <p className="text-gray-700 mb-1">Studio: {title.studio.name}</p>}
|
|
||||||
|
{title.studio && (
|
||||||
|
<p className="text-gray-700 mb-1">
|
||||||
|
Studio:{" "}
|
||||||
|
{title.studio.id ? (
|
||||||
|
<Link
|
||||||
|
to={`/studios/${title.studio.id}`}
|
||||||
|
className="text-blue-600 hover:underline"
|
||||||
|
>
|
||||||
|
{title.studio.name}
|
||||||
|
</Link>
|
||||||
|
) : (
|
||||||
|
title.studio.name
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{title.title_status && <p className="text-gray-700 mb-1">Status: {title.title_status}</p>}
|
{title.title_status && <p className="text-gray-700 mb-1">Status: {title.title_status}</p>}
|
||||||
|
|
||||||
{title.rating !== undefined && (
|
{title.rating !== undefined && (
|
||||||
<p className="text-gray-700 mb-1">
|
<p className="text-gray-700 mb-1">
|
||||||
Rating: {title.rating} ({title.rating_count} votes)
|
Rating: {title.rating} ({title.rating_count} votes)
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{title.release_year && (
|
{title.release_year && (
|
||||||
<p className="text-gray-700 mb-1">
|
<p className="text-gray-700 mb-1">
|
||||||
Released: {title.release_year} {title.release_season || ""}
|
Released: {title.release_year} {title.release_season || ""}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{title.episodes_aired !== undefined && (
|
{title.episodes_aired !== undefined && (
|
||||||
<p className="text-gray-700 mb-1">
|
<p className="text-gray-700 mb-1">
|
||||||
Episodes: {title.episodes_aired}/{title.episodes_all}
|
Episodes: {title.episodes_aired}/{title.episodes_all}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{title.tags && title.tags.length > 0 && (
|
{title.tags && title.tags.length > 0 && (
|
||||||
<p className="text-gray-700 mb-1">
|
<p className="text-gray-700 mb-1">
|
||||||
Tags: {getTagsString()}
|
Tags: {getTagsString()}
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,7 @@ export default function UserPage({ userId }: UserPageProps) {
|
||||||
: "";
|
: "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await DefaultService.getUsersTitles(
|
const result = await DefaultService.getUserTitles(
|
||||||
id,
|
id,
|
||||||
cursorStr,
|
cursorStr,
|
||||||
sort,
|
sort,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue