feat: added title page
This commit is contained in:
parent
68294dd13c
commit
4c643d80bb
4 changed files with 145 additions and 2 deletions
|
|
@ -2,6 +2,7 @@ import React from "react";
|
||||||
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
import { BrowserRouter as Router, Routes, Route } from "react-router-dom";
|
||||||
import UsersIdPage from "./pages/UsersIdPage/UsersIdPage";
|
import UsersIdPage from "./pages/UsersIdPage/UsersIdPage";
|
||||||
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
import TitlesPage from "./pages/TitlesPage/TitlesPage";
|
||||||
|
import TitlePage from "./pages/TitlePage/TitlePage";
|
||||||
import { LoginPage } from "./pages/LoginPage/LoginPage";
|
import { LoginPage } from "./pages/LoginPage/LoginPage";
|
||||||
import { Header } from "./components/Header/Header";
|
import { Header } from "./components/Header/Header";
|
||||||
|
|
||||||
|
|
@ -24,7 +25,9 @@ const App: React.FC = () => {
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Route path="/users/:id" element={<UsersIdPage />} />
|
<Route path="/users/:id" element={<UsersIdPage />} />
|
||||||
|
|
||||||
<Route path="/titles" element={<TitlesPage />} />
|
<Route path="/titles" element={<TitlesPage />} />
|
||||||
|
<Route path="/titles/:id" element={<TitlePage />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</Router>
|
</Router>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export type OpenAPIConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OpenAPI: OpenAPIConfig = {
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
BASE: '/api/v1',
|
BASE: 'http://10.1.0.65:8081/api/v1',
|
||||||
VERSION: '1.0.0',
|
VERSION: '1.0.0',
|
||||||
WITH_CREDENTIALS: false,
|
WITH_CREDENTIALS: false,
|
||||||
CREDENTIALS: 'include',
|
CREDENTIALS: 'include',
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ export type OpenAPIConfig = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const OpenAPI: OpenAPIConfig = {
|
export const OpenAPI: OpenAPIConfig = {
|
||||||
BASE: '/auth',
|
BASE: 'http://10.1.0.65:8081/auth',
|
||||||
VERSION: '1.0.0',
|
VERSION: '1.0.0',
|
||||||
WITH_CREDENTIALS: false,
|
WITH_CREDENTIALS: false,
|
||||||
CREDENTIALS: 'include',
|
CREDENTIALS: 'include',
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,140 @@
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
import { useParams } 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: <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() {
|
||||||
|
const params = useParams();
|
||||||
|
const titleId = Number(params.id);
|
||||||
|
|
||||||
|
const [title, setTitle] = useState<Title | null>(null);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const [userStatus, setUserStatus] = useState<UserTitleStatus | null>(null);
|
||||||
|
const [updatingStatus, setUpdatingStatus] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const fetchTitle = async () => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const data = await DefaultService.getTitle(titleId, "all");
|
||||||
|
setTitle(data);
|
||||||
|
setError(null);
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(err);
|
||||||
|
setError(err?.message || "Failed to fetch title");
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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(", ");
|
||||||
|
|
||||||
|
if (loading) return <div className="mt-20 font-medium text-black">Loading title...</div>;
|
||||||
|
if (error) return <div className="mt-20 text-red-600 font-medium">{error}</div>;
|
||||||
|
if (!title) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<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 items-center">
|
||||||
|
<img
|
||||||
|
src={title.poster?.image_path || "/default-poster.png"}
|
||||||
|
alt={title.title_names?.en?.[0] || "Title poster"}
|
||||||
|
className="w-48 h-72 object-cover rounded-lg mb-4"
|
||||||
|
/>
|
||||||
|
|
||||||
|
{/* Статус кнопки с иконками */}
|
||||||
|
<div className="flex gap-2 mt-2 flex-wrap justify-center">
|
||||||
|
{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 className="flex-1 flex flex-col">
|
||||||
|
<h1 className="text-3xl font-bold mb-2">
|
||||||
|
{title.title_names?.en?.[0] || "Untitled"}
|
||||||
|
</h1>
|
||||||
|
{title.studio && <p className="text-gray-700 mb-1">Studio: {title.studio.name}</p>}
|
||||||
|
{title.title_status && <p className="text-gray-700 mb-1">Status: {title.title_status}</p>}
|
||||||
|
{title.rating !== undefined && (
|
||||||
|
<p className="text-gray-700 mb-1">
|
||||||
|
Rating: {title.rating} ({title.rating_count} votes)
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{title.release_year && (
|
||||||
|
<p className="text-gray-700 mb-1">
|
||||||
|
Released: {title.release_year} {title.release_season || ""}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{title.episodes_aired !== undefined && (
|
||||||
|
<p className="text-gray-700 mb-1">
|
||||||
|
Episodes: {title.episodes_aired}/{title.episodes_all}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{title.tags && title.tags.length > 0 && (
|
||||||
|
<p className="text-gray-700 mb-1">
|
||||||
|
Tags: {getTagsString()}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue