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:
2026-06-03 22:06:01 -05:00
parent 730c748f5f
commit 6b4c0c43f8
137 changed files with 20319 additions and 2340 deletions
@@ -0,0 +1,119 @@
import { useEffect, useMemo, useState } from "react";
import { useTranslation } from "react-i18n-lite";
import { useAnnotations } from "@/context/AnnotationsContext";
import type { MarkerEntry } from "@/types";
import {
CollapsibleMarkerPassage,
CollapsibleMarkerThought,
} from "@/components/annotations/CollapsibleMarkerText";
interface MarkerThreadsViewProps {
ebookIdFilter?: string;
onGoToPassage: (ebookId: number, epubCfi: string) => void;
}
export function MarkerThreadsView({
ebookIdFilter,
onGoToPassage,
}: MarkerThreadsViewProps) {
const { t } = useTranslation();
const { markersByBook, loadBookmarks, removeBookmark, state } = useAnnotations();
const [collapsed, setCollapsed] = useState<Record<number, boolean>>({});
useEffect(() => {
void loadBookmarks(ebookIdFilter);
}, [loadBookmarks, ebookIdFilter]);
const groups = useMemo(() => {
if (!ebookIdFilter) return markersByBook;
const id = Number(ebookIdFilter);
return markersByBook.filter((g) => g.ebookId === id);
}, [markersByBook, ebookIdFilter]);
if (state.bookmarksLoading) {
return <div className="annotations-loading">{t("annotations.loading")}</div>;
}
if (groups.length === 0) {
return (
<div className="annotations-empty">
<p>{t("annotations.empty")}</p>
<p style={{ fontSize: 14, color: "#6b7280", marginTop: 8 }}>{t("annotations.selectTextHint")}</p>
</div>
);
}
const toggleBook = (ebookId: number) => {
setCollapsed((prev) => ({ ...prev, [ebookId]: !prev[ebookId] }));
};
return (
<div className="marker-books-list">
{groups.map((group) => {
const isCollapsed = collapsed[group.ebookId] ?? false;
return (
<section key={group.ebookId} className="marker-book-group">
<button
type="button"
className="marker-book-header"
onClick={() => toggleBook(group.ebookId)}
>
<span className="marker-book-title">{group.ebookTitle}</span>
<span className="marker-book-count">
{t("annotations.markerCount", { count: String(group.markers.length) })}
</span>
<span className="marker-book-chevron">{isCollapsed ? "▸" : "▾"}</span>
</button>
{!isCollapsed && (
<ul className="marker-thread-list">
{group.markers.map((m: MarkerEntry) => (
<li key={m.id} className="marker-thread">
<div className="marker-thread-meta">
{m.chapter_title ? (
<span className="marker-thread-chapter">{m.chapter_title}</span>
) : (
<span className="marker-thread-chapter">
{t("annotations.chapterIndex", { index: String(m.chapter_index + 1) })}
</span>
)}
</div>
{m.location_text && (
<CollapsibleMarkerPassage
text={m.location_text}
highlightColor={m.highlight_color}
/>
)}
{m.content ? (
<CollapsibleMarkerThought
text={m.content}
className="marker-thread-thought marker-thread-reply"
/>
) : (
<span className="annotation-kind-badge bookmark-badge">{t("annotations.bookmarkOnly")}</span>
)}
<div className="annotation-actions">
<button
type="button"
className="btn btn-sm"
onClick={() => onGoToPassage(m.ebook_id, m.epub_cfi)}
>
{t("annotations.goToPassage")}
</button>
<button
type="button"
className="btn btn-sm btn-danger"
onClick={() => removeBookmark(m.id)}
>
{t("common.delete")}
</button>
</div>
</li>
))}
</ul>
)}
</section>
);
})}
</div>
);
}