feat: titles page
Some checks failed
Build and Deploy Go App / build (push) Failing after 11m8s
Build and Deploy Go App / deploy (push) Has been skipped

This commit is contained in:
nihonium 2025-11-18 05:15:38 +03:00
parent 6836cfa057
commit b976c35b8e
Signed by: nihonium
GPG key ID: 0251623741027CFC
44 changed files with 1539 additions and 107 deletions

View file

@ -0,0 +1,52 @@
import React from "react";
interface ListViewProps<TItem> {
hook: ReturnType<typeof import("./useListView.tsx").useListView<TItem>>;
renderHorizontal: (item: TItem) => React.ReactNode;
renderSquare: (item: TItem) => React.ReactNode;
}
export function ListView<TItem>({
hook,
renderHorizontal,
renderSquare
}: ListViewProps<TItem>) {
const { items, search, setSearch, viewMode, setViewMode, loadMore, hasMore } = hook;
return (
<div>
{/* Search + Layout Switcher */}
<div style={{ display: "flex", gap: 8, marginBottom: 16 }}>
<input
placeholder="Search..."
value={search}
onChange={e => setSearch(e.target.value)}
/>
<button onClick={() => setViewMode("horizontal")}>Horizontal</button>
<button onClick={() => setViewMode("square")}>Square</button>
</div>
{/* Items */}
<div
style={{
display: "grid",
gridTemplateColumns: viewMode === "square" ? "repeat(auto-fill, 160px)" : "1fr",
gap: 12
}}
>
{items.map(item =>
viewMode === "horizontal"
? renderHorizontal(item)
: renderSquare(item)
)}
</div>
{hasMore && (
<button onClick={loadMore} style={{ marginTop: 16 }}>
Load More
</button>
)}
</div>
);
}

View file

@ -0,0 +1,37 @@
import { useState, useEffect } from "react";
import type { FetchFunction } from "../../types/list";
export function useListView<TItem>(fetchFn: FetchFunction<TItem>) {
const [items, setItems] = useState<TItem[]>([]);
const [cursor, setCursor] = useState<string | undefined>();
const [search, setSearch] = useState("");
const [viewMode, setViewMode] = useState<"horizontal" | "square">("horizontal");
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
loadItems(true);
}, [search]);
const loadItems = async (reset = false) => {
setIsLoading(true);
const result = await fetchFn({
search,
cursor: reset ? undefined : cursor,
});
setItems(prev => reset ? result.items : [...prev, ...result.items]);
setCursor(result.nextCursor);
setIsLoading(false);
};
return {
items,
search,
setSearch,
viewMode,
setViewMode,
loadMore: () => loadItems(),
hasMore: Boolean(cursor),
isLoading,
};
}

View file

@ -1,97 +0,0 @@
.container {
display: flex;
justify-content: center;
align-items: flex-start;
padding: 3rem 1rem;
background-color: #f5f6fa;
min-height: 100vh;
font-family: "Inter", sans-serif;
}
.card {
background-color: #ffffff;
border-radius: 1rem;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.1);
padding: 2rem;
max-width: 400px;
width: 100%;
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
}
.avatar {
margin-bottom: 1.5rem;
}
.avatarImg {
width: 120px;
height: 120px;
border-radius: 50%;
object-fit: cover;
border: 3px solid #4a90e2;
}
.avatarPlaceholder {
width: 120px;
height: 120px;
border-radius: 50%;
background-color: #dcdde1;
display: flex;
align-items: center;
justify-content: center;
font-size: 3rem;
color: #4a4a4a;
font-weight: bold;
border: 3px solid #4a90e2;
}
.info {
display: flex;
flex-direction: column;
align-items: center;
}
.name {
font-size: 1.8rem;
font-weight: 700;
margin: 0.25rem 0;
color: #2f3640;
}
.nickname {
font-size: 1rem;
color: #718093;
margin-bottom: 1rem;
}
.desc {
font-size: 1rem;
color: #353b48;
margin-bottom: 1rem;
}
.created {
font-size: 0.9rem;
color: #7f8fa6;
}
.loader {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
font-size: 1.5rem;
color: #4a90e2;
}
.error {
display: flex;
justify-content: center;
align-items: center;
height: 80vh;
color: #e84118;
font-weight: 600;
font-size: 1.2rem;
}

View file

@ -1,64 +0,0 @@
import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; // <-- import
import { DefaultService } from "../../api/services/DefaultService";
import type { User } from "../../api/models/User";
import styles from "./UserPage.module.css";
const UserPage: React.FC = () => {
const { id } = useParams<{ id: string }>(); // <-- get user id from URL
const [user, setUser] = useState<User | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!id) return;
const getUserInfo = async () => {
try {
const userInfo = await DefaultService.getUsers(id, "all"); // <-- use dynamic id
setUser(userInfo);
} catch (err) {
console.error(err);
setError("Failed to fetch user info.");
} finally {
setLoading(false);
}
};
getUserInfo();
}, [id]);
if (loading) return <div className={styles.loader}>Loading...</div>;
if (error) return <div className={styles.error}>{error}</div>;
if (!user) return <div className={styles.error}>User not found.</div>;
return (
<div className={styles.container}>
<div className={styles.card}>
<div className={styles.avatar}>
{user.avatar_id ? (
<img
src={`/images/${user.avatar_id}.png`}
alt="User Avatar"
className={styles.avatarImg}
/>
) : (
<div className={styles.avatarPlaceholder}>
{user.disp_name?.[0] || "U"}
</div>
)}
</div>
<div className={styles.info}>
<h1 className={styles.name}>{user.disp_name || user.nickname}</h1>
<p className={styles.nickname}>@{user.nickname}</p>
{user.user_desc && <p className={styles.desc}>{user.user_desc}</p>}
<p className={styles.created}>
Joined: {new Date(user.creation_date).toLocaleDateString()}
</p>
</div>
</div>
</div>
);
};
export default UserPage;

View file

@ -0,0 +1,22 @@
import type { Title } from "../../api/models/Title";
export function TitleCardHorizontal({ title }: { title: Title }) {
return (
<div style={{
display: "flex",
gap: 12,
padding: 12,
border: "1px solid #ddd",
borderRadius: 8
}}>
{title.posterUrl && (
<img src={title.posterUrl} width={80} />
)}
<div>
<h3>{title.name}</h3>
<p>{title.year} · {title.season} · Rating: {title.rating}</p>
<p>Status: {title.status}</p>
</div>
</div>
);
}

View file

@ -0,0 +1,22 @@
// TitleCardSquare.tsx
import type { Title } from "../../api/models/Title";
export function TitleCardSquare({ title }: { title: Title }) {
return (
<div style={{
width: 160,
border: "1px solid #ddd",
padding: 8,
borderRadius: 8,
textAlign: "center"
}}>
{title.posterUrl && (
<img src={title.posterUrl} width={140} />
)}
<div>
<h4>{title.name}</h4>
<small>{title.year} {title.rating}</small>
</div>
</div>
);
}