Archived
feat: uv config other feats
- add uv configuration for the backend - update frontend to make auth work - add new auth endpoints - add bookmars feat - add reader feat
This commit is contained in:
@@ -0,0 +1,140 @@
|
||||
.shelf {
|
||||
position: fixed;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 90;
|
||||
background: #fff;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
box-shadow: 0 -4px 24px rgba(0, 0, 0, 0.08);
|
||||
transition: max-height 0.25s ease;
|
||||
}
|
||||
|
||||
.shelfCollapsed {
|
||||
max-height: 52px;
|
||||
}
|
||||
|
||||
.shelfExpanded {
|
||||
max-height: min(42vh, 320px);
|
||||
}
|
||||
|
||||
.shelfHandle {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
border: none;
|
||||
background: #fafafa;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.shelfHandle:hover {
|
||||
background: #f3f4f6;
|
||||
}
|
||||
|
||||
.shelfHandleTitle {
|
||||
font-weight: 700;
|
||||
font-size: 14px;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.shelfHandleCount {
|
||||
font-size: 13px;
|
||||
color: #6b7280;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.shelfChevron {
|
||||
font-size: 14px;
|
||||
color: #9333ea;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.shelfBody {
|
||||
overflow-y: auto;
|
||||
max-height: calc(min(42vh, 320px) - 52px);
|
||||
padding: 12px 16px 16px;
|
||||
border-top: 1px solid #f3f4f6;
|
||||
}
|
||||
|
||||
.shelfGrid {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
overflow-x: auto;
|
||||
padding-bottom: 4px;
|
||||
scroll-snap-type: x proximity;
|
||||
}
|
||||
|
||||
.shelfCard {
|
||||
flex: 0 0 120px;
|
||||
scroll-snap-align: start;
|
||||
cursor: pointer;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid #e5e7eb;
|
||||
transition: transform 0.15s, box-shadow 0.15s;
|
||||
}
|
||||
|
||||
.shelfCard:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.shelfCover {
|
||||
position: relative;
|
||||
height: 160px;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
.shelfCover img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.shelfCoverPlaceholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
font-size: 32px;
|
||||
}
|
||||
|
||||
.shelfBadge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
background: #f3e8ff;
|
||||
color: #9333ea;
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 2px 6px;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.shelfMeta {
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.shelfTitle {
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.shelfAuthor {
|
||||
margin: 2px 0 0;
|
||||
font-size: 11px;
|
||||
color: #6b7280;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import styles from "./FinishedBooksShelf.module.css";
|
||||
|
||||
export interface ShelfBook {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
cover_image: string | null;
|
||||
format: string;
|
||||
genre: string;
|
||||
}
|
||||
|
||||
interface FinishedBooksShelfProps {
|
||||
books: ShelfBook[];
|
||||
defaultExpanded?: boolean;
|
||||
onOpenBook: (book: ShelfBook) => void;
|
||||
onContextMenu: (e: React.MouseEvent, book: ShelfBook) => void;
|
||||
}
|
||||
|
||||
export function FinishedBooksShelf({
|
||||
books,
|
||||
defaultExpanded = false,
|
||||
onOpenBook,
|
||||
onContextMenu,
|
||||
}: FinishedBooksShelfProps) {
|
||||
const { t } = useTranslation();
|
||||
const [expanded, setExpanded] = useState(defaultExpanded);
|
||||
|
||||
useEffect(() => {
|
||||
if (defaultExpanded) setExpanded(true);
|
||||
}, [defaultExpanded]);
|
||||
|
||||
if (books.length === 0) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.shelf} ${expanded ? styles.shelfExpanded : styles.shelfCollapsed}`}
|
||||
aria-label={t("library.finishedSection")}
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.shelfHandle}
|
||||
onClick={() => setExpanded((v) => !v)}
|
||||
aria-expanded={expanded}
|
||||
>
|
||||
<span className={styles.shelfHandleTitle}>{t("library.finishedSection")}</span>
|
||||
<span className={styles.shelfHandleCount}>
|
||||
{t("library.finishedCount", { count: String(books.length) })}
|
||||
</span>
|
||||
<span className={styles.shelfChevron} aria-hidden>
|
||||
{expanded ? "▾" : "▴"}
|
||||
</span>
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className={styles.shelfBody}>
|
||||
<div className={styles.shelfGrid}>
|
||||
{books.map((book) => (
|
||||
<div
|
||||
key={book.id}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={styles.shelfCard}
|
||||
onClick={() => onOpenBook(book)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
onOpenBook(book);
|
||||
}
|
||||
}}
|
||||
onContextMenu={(e) => onContextMenu(e, book)}
|
||||
>
|
||||
<div className={styles.shelfCover}>
|
||||
{book.cover_image ? (
|
||||
<img src={book.cover_image} alt="" />
|
||||
) : (
|
||||
<span className={styles.shelfCoverPlaceholder}>📖</span>
|
||||
)}
|
||||
<span className={styles.shelfBadge}>{t("library.readingStatus.finished")}</span>
|
||||
</div>
|
||||
<div className={styles.shelfMeta}>
|
||||
<p className={styles.shelfTitle}>{book.title}</p>
|
||||
<p className={styles.shelfAuthor}>{book.author}</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
import { useTranslation } from "react-i18n-lite";
|
||||
import { readingStatusKey } from "@/locales";
|
||||
import styles from "../../pages/Library.module.css";
|
||||
|
||||
export const LIBRARY_STATUS_COLORS: Record<string, { bg: string; text: string }> = {
|
||||
want_to_read: { bg: "#dbeafe", text: "#1d4ed8" },
|
||||
reading: { bg: "#dcfce7", text: "#16a34a" },
|
||||
finished: { bg: "#f3e8ff", text: "#9333ea" },
|
||||
dnf: { bg: "#fef3c7", text: "#b45309" },
|
||||
};
|
||||
|
||||
export interface LibraryBookCardData {
|
||||
id: number;
|
||||
title: string;
|
||||
author: string;
|
||||
genre: string;
|
||||
format: string;
|
||||
cover_image: string | null;
|
||||
reading_status: string;
|
||||
progressPercent: number | null;
|
||||
}
|
||||
|
||||
function badgeLabel(
|
||||
book: LibraryBookCardData,
|
||||
t: (key: string, params?: Record<string, string>) => string,
|
||||
): string {
|
||||
if (book.reading_status === "reading" && book.progressPercent != null && book.progressPercent > 0) {
|
||||
return t("library.readingStatus.readingWithProgress", {
|
||||
percent: String(book.progressPercent),
|
||||
});
|
||||
}
|
||||
return t(readingStatusKey(book.reading_status));
|
||||
}
|
||||
|
||||
interface LibraryBookCardProps {
|
||||
book: LibraryBookCardData;
|
||||
isMobile: boolean;
|
||||
onOpen: () => void;
|
||||
onContextMenu: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
export function LibraryBookCard({
|
||||
book,
|
||||
isMobile,
|
||||
onOpen,
|
||||
onContextMenu,
|
||||
}: LibraryBookCardProps) {
|
||||
const { t } = useTranslation();
|
||||
const sc = LIBRARY_STATUS_COLORS[book.reading_status] ?? { bg: "#f3f4f6", text: "#6b7280" };
|
||||
const showProgressBar =
|
||||
book.reading_status === "reading" &&
|
||||
book.progressPercent != null &&
|
||||
book.progressPercent > 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={onOpen}
|
||||
onContextMenu={onContextMenu}
|
||||
className={styles.bookCard}
|
||||
style={{ display: isMobile ? "flex" : "block" }}
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: isMobile ? 80 : "100%",
|
||||
height: isMobile ? 120 : 180,
|
||||
background: "#f0f0f0",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
position: "relative",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{book.cover_image ? (
|
||||
<img
|
||||
src={book.cover_image}
|
||||
alt={book.title}
|
||||
style={{ width: "100%", height: "100%", objectFit: "cover" }}
|
||||
/>
|
||||
) : (
|
||||
<span style={{ fontSize: isMobile ? 32 : 48 }}>📖</span>
|
||||
)}
|
||||
{showProgressBar && (
|
||||
<div className={styles.coverProgressBar}>
|
||||
<div
|
||||
className={styles.coverProgressFill}
|
||||
style={{ width: `${book.progressPercent}%` }}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{!isMobile && (
|
||||
<span
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 8,
|
||||
right: 8,
|
||||
background: sc.bg,
|
||||
color: sc.text,
|
||||
fontSize: 11,
|
||||
fontWeight: 600,
|
||||
padding: "2px 8px",
|
||||
borderRadius: 999,
|
||||
lineHeight: "18px",
|
||||
maxWidth: "calc(100% - 16px)",
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{badgeLabel(book, t)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div style={{ padding: isMobile ? "8px 12px" : 12, flex: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "flex-start",
|
||||
justifyContent: "space-between",
|
||||
gap: 8,
|
||||
}}
|
||||
>
|
||||
<div style={{ minWidth: 0, flex: 1 }}>
|
||||
<h3
|
||||
style={{
|
||||
fontSize: 14,
|
||||
fontWeight: 600,
|
||||
color: "#1f2937",
|
||||
margin: 0,
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{book.title}
|
||||
</h3>
|
||||
<p style={{ fontSize: 12, color: "#6b7280", margin: "2px 0" }}>
|
||||
{book.author || t("common.unknownAuthor")}
|
||||
</p>
|
||||
</div>
|
||||
{isMobile && (
|
||||
<span
|
||||
style={{
|
||||
background: sc.bg,
|
||||
color: sc.text,
|
||||
fontSize: 10,
|
||||
fontWeight: 600,
|
||||
padding: "2px 6px",
|
||||
borderRadius: 999,
|
||||
whiteSpace: "nowrap",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
{badgeLabel(book, t)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{book.genre && (
|
||||
<span
|
||||
style={{
|
||||
fontSize: 11,
|
||||
color: "#4f46e5",
|
||||
background: "#eef2ff",
|
||||
padding: "1px 6px",
|
||||
borderRadius: 4,
|
||||
display: "inline-block",
|
||||
marginTop: 4,
|
||||
}}
|
||||
>
|
||||
{book.genre}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user