diff --git a/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx b/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx
new file mode 100644
index 0000000..ad7d5df
--- /dev/null
+++ b/modules/frontend/src/components/cards/UserTitleCardHorizontal.tsx
@@ -0,0 +1,22 @@
+import type { UserTitle } from "../../api";
+
+export function UserTitleCardHorizontal({ title }: { title: UserTitle }) {
+ return (
+
+ {title.title?.poster?.image_path && (
+

+ )}
+
+
{title.title?.title_names["en"]}
+
{title.title?.release_year} · {title.title?.release_season} · Rating: {title.title?.rating}
+
Status: {title.title?.title_status}
+
+
+ );
+}
diff --git a/modules/frontend/src/components/cards/UserTitleCardSquare.tsx b/modules/frontend/src/components/cards/UserTitleCardSquare.tsx
new file mode 100644
index 0000000..edcf1d5
--- /dev/null
+++ b/modules/frontend/src/components/cards/UserTitleCardSquare.tsx
@@ -0,0 +1,22 @@
+import type { UserTitle } from "../../api";
+
+export function UserTitleCardSquare({ title }: { title: UserTitle }) {
+ return (
+
+ {title.title?.poster?.image_path && (
+

+ )}
+
+
{title.title?.title_names["en"]}
+ {title.status}
+ {title.title?.release_year} • {title.title?.rating}
+
+
+ );
+}
diff --git a/modules/frontend/src/pages/LoginPage/LoginPage.tsx b/modules/frontend/src/pages/LoginPage/LoginPage.tsx
index dcd6965..89ee88c 100644
--- a/modules/frontend/src/pages/LoginPage/LoginPage.tsx
+++ b/modules/frontend/src/pages/LoginPage/LoginPage.tsx
@@ -18,17 +18,19 @@ export const LoginPage: React.FC = () => {
try {
if (isLogin) {
const res = await AuthService.postAuthSignIn({ nickname, pass: password });
- if (res.success) {
- // TODO: сохранить JWT в localStorage/cookie
- console.log("Logged in user id:", res.user_id);
- navigate("/"); // редирект после успешного входа
+ if (res.user_id && res.user_name) {
+ // Сохраняем user_id и username в localStorage
+ localStorage.setItem("userId", res.user_id);
+ localStorage.setItem("username", res.user_name);
+
+ navigate("/profile"); // редирект на профиль
} else {
setError(res.error || "Login failed");
}
} else {
+ // SignUp оставляем без сохранения данных
const res = await AuthService.postAuthSignUp({ nickname, pass: password });
- if (res.success) {
- console.log("User signed up with id:", res.user_id);
+ if (res.user_id) {
setIsLogin(true); // переключаемся на login после регистрации
} else {
setError(res.error || "Sign up failed");
diff --git a/modules/frontend/src/pages/UserPage/UserPage.tsx b/modules/frontend/src/pages/UserPage/UserPage.tsx
index 52c5574..2e39e6b 100644
--- a/modules/frontend/src/pages/UserPage/UserPage.tsx
+++ b/modules/frontend/src/pages/UserPage/UserPage.tsx
@@ -35,9 +35,9 @@ const UserPage: React.FC = () => {
- {user.avatar_id ? (
+ {user.image?.image_path ? (

diff --git a/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx
new file mode 100644
index 0000000..342f22c
--- /dev/null
+++ b/modules/frontend/src/pages/UsersIdPage/UsersIdPage.tsx
@@ -0,0 +1,183 @@
+// pages/UserPage/UserPage.tsx
+import { useEffect, useState } from "react";
+import { useParams } from "react-router-dom";
+import { DefaultService } from "../../api/services/DefaultService";
+import { SearchBar } from "../../components/SearchBar/SearchBar";
+import { TitlesSortBox } from "../../components/TitlesSortBox/TitlesSortBox";
+import { LayoutSwitch } from "../../components/LayoutSwitch/LayoutSwitch";
+import { ListView } from "../../components/ListView/ListView";
+import { UserTitleCardSquare } from "../../components/cards/UserTitleCardSquare";
+import { UserTitleCardHorizontal } from "../../components/cards/UserTitleCardHorizontal";
+import type { User, UserTitle, CursorObj, TitleSort } from "../../api";
+
+const PAGE_SIZE = 10;
+
+type UsersIdPageProps = {
+ userId?: string;
+};
+
+export default function UsersIdPage({ userId }: UsersIdPageProps) {
+ const params = useParams();
+ const id = userId || params?.id;
+
+ const [user, setUser] = useState
(null);
+ const [loadingUser, setLoadingUser] = useState(true);
+ const [errorUser, setErrorUser] = useState(null);
+
+ // Для списка тайтлов
+ const [titles, setTitles] = useState([]);
+ const [nextPage, setNextPage] = useState([]);
+ const [cursor, setCursor] = useState(null);
+ const [loadingTitles, setLoadingTitles] = useState(true);
+ const [loadingMore, setLoadingMore] = useState(false);
+ const [search, setSearch] = useState("");
+ const [sort, setSort] = useState("id");
+ const [sortForward, setSortForward] = useState(true);
+ const [layout, setLayout] = useState<"square" | "horizontal">("square");
+
+ // --- Получение данных пользователя ---
+ useEffect(() => {
+ const fetchUser = async () => {
+ if (!id) return;
+ setLoadingUser(true);
+ try {
+ const result = await DefaultService.getUsers(id, "all");
+ setUser(result);
+ setErrorUser(null);
+ } catch (err: any) {
+ console.error(err);
+ setErrorUser(err?.message || "Failed to fetch user data");
+ } finally {
+ setLoadingUser(false);
+ }
+ };
+ fetchUser();
+ }, [id]);
+
+ // --- Получение списка тайтлов пользователя ---
+ const fetchPage = async (cursorObj: CursorObj | null) => {
+ if (!id) return { items: [], nextCursor: null };
+ const cursorStr = cursorObj
+ ? btoa(JSON.stringify(cursorObj)).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "")
+ : "";
+
+ try {
+ const result = await DefaultService.getUsersTitles(
+ id,
+ cursorStr,
+ sort,
+ sortForward,
+ search.trim() || undefined,
+ undefined, // status фильтр, можно добавить
+ undefined, // watchStatus
+ undefined, // rating
+ undefined, // myRate
+ undefined, // releaseYear
+ undefined, // releaseSeason
+ PAGE_SIZE,
+ "all"
+ );
+
+ if (!result?.data?.length) return { items: [], nextCursor: null };
+
+ return { items: result.data, nextCursor: result.cursor ?? null };
+ } catch (err: any) {
+ if (err.status === 204) return { items: [], nextCursor: null };
+ throw err;
+ }
+ };
+
+ // Инициализация: загружаем сразу две страницы
+ useEffect(() => {
+ const initLoad = async () => {
+ setLoadingTitles(true);
+ setTitles([]);
+ setNextPage([]);
+ setCursor(null);
+
+ const firstPage = await fetchPage(null);
+ const secondPage = firstPage.nextCursor ? await fetchPage(firstPage.nextCursor) : { items: [], nextCursor: null };
+
+ setTitles(firstPage.items);
+ setNextPage(secondPage.items);
+ setCursor(secondPage.nextCursor);
+ setLoadingTitles(false);
+ };
+ initLoad();
+ }, [id, search, sort, sortForward]);
+
+ const handleLoadMore = async () => {
+ if (nextPage.length === 0) {
+ setLoadingMore(false);
+ return;
+ }
+ setLoadingMore(true);
+
+ setTitles(prev => [...prev, ...nextPage]);
+ setNextPage([]);
+
+ if (cursor) {
+ try {
+ const next = await fetchPage(cursor);
+ if (next.items.length > 0) setNextPage(next.items);
+ setCursor(next.nextCursor);
+ } catch (err) {
+ console.error(err);
+ }
+ }
+
+ setLoadingMore(false);
+ };
+
+ // const getAvatarUrl = (avatarId?: number) => (avatarId ? `/api/images/${avatarId}` : "/default-avatar.png");
+
+ return (
+
+
+ {/* --- Карточка пользователя --- */}
+ {loadingUser &&
Loading user...
}
+ {errorUser &&
{errorUser}
}
+ {user && (
+
+

+
{user.disp_name || user.nickname}
+ {user.mail &&
{user.mail}
}
+ {user.user_desc &&
{user.user_desc}
}
+ {user.creation_date &&
Registered: {new Date(user.creation_date).toLocaleDateString()}
}
+
+ )}
+
+ {/* --- Панель поиска, сортировки и лейаута --- */}
+
+
+
+
+
+
+ {/* --- Список тайтлов --- */}
+ {loadingTitles &&
Loading titles...
}
+ {!loadingTitles && titles.length === 0 &&
No titles found.
}
+
+ {titles.length > 0 && (
+ <>
+
+ items={titles}
+ layout={layout}
+ hasMore={!!cursor || nextPage.length > 1}
+ loadingMore={loadingMore}
+ onLoadMore={handleLoadMore}
+ renderItem={(title, layout) =>
+ layout === "square" ? :
+ }
+ />
+
+ {!cursor && nextPage.length === 0 && (
+
+ Результатов больше нет, было найдено {titles.length} тайтлов.
+
+ )}
+ >
+ )}
+
+ );
+}