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
@@ -3,38 +3,44 @@
* the reading experience: theme, font, sizing, orientation.
*/
import { useState } from "react";
import type { KeyboardEvent as ReactKeyboardEvent } from "react";
import { useTranslation } from "react-i18n-lite";
import type {
FontFamily,
OrientationLock,
ReadingSettings,
ThemePreset,
} from "../../types/reader";
import type { SettingsPersistMode } from "../../hooks/useReadingSettings";
interface ReadingSettingsPanelProps {
settings: ReadingSettings;
isOpen: boolean;
onClose: () => void;
onUpdate: (partial: Partial<ReadingSettings>) => Promise<void>;
onUpdate: (
partial: Partial<ReadingSettings>,
persist?: SettingsPersistMode,
) => void;
onFlush: () => Promise<void>;
}
const THEME_OPTIONS: { value: ThemePreset; label: string }[] = [
{ value: "sepia", label: "Sepia" },
{ value: "dark", label: "Dark" },
{ value: "light", label: "Light" },
{ value: "paper", label: "Paper" },
const THEME_OPTIONS: { value: ThemePreset; labelKey: string }[] = [
{ value: "sepia", labelKey: "reader.themeSepia" },
{ value: "dark", labelKey: "reader.themeDark" },
{ value: "light", labelKey: "reader.themeLight" },
{ value: "paper", labelKey: "reader.themePaper" },
];
const FONT_OPTIONS: { value: FontFamily; label: string }[] = [
{ value: "sans-serif", label: "Sans-serif" },
{ value: "serif", label: "Serif" },
{ value: "monospace", label: "Monospace" },
const FONT_OPTIONS: { value: FontFamily; labelKey: string }[] = [
{ value: "sans-serif", labelKey: "reader.fontSans" },
{ value: "serif", labelKey: "reader.fontSerif" },
{ value: "monospace", labelKey: "reader.fontMonospace" },
];
const ORIENTATION_OPTIONS: { value: OrientationLock; label: string }[] = [
{ value: "auto", label: "Auto" },
{ value: "portrait", label: "Portrait" },
{ value: "landscape", label: "Landscape" },
const ORIENTATION_OPTIONS: { value: OrientationLock; labelKey: string }[] = [
{ value: "auto", labelKey: "reader.orientationAuto" },
{ value: "portrait", labelKey: "reader.orientationPortrait" },
{ value: "landscape", labelKey: "reader.orientationLandscape" },
];
export default function ReadingSettingsPanel({
@@ -42,46 +48,46 @@ export default function ReadingSettingsPanel({
isOpen,
onClose,
onUpdate,
onFlush,
}: ReadingSettingsPanelProps) {
const [saving, setSaving] = useState<Record<string, boolean>>({});
const { t } = useTranslation();
const handleChange = async (
key: keyof ReadingSettings,
value: string | number
) => {
setSaving((prev) => ({ ...prev, [key]: true }));
try {
await onUpdate({ [key]: value as never });
} finally {
setSaving((prev) => ({ ...prev, [key]: false }));
}
const handleClose = () => {
void onFlush();
onClose();
};
const handleSlider = (key: keyof ReadingSettings, value: number) => {
onUpdate({ [key]: value } as Partial<ReadingSettings>, "debounced");
};
const handleDiscrete = (key: keyof ReadingSettings, value: string | number) => {
onUpdate({ [key]: value } as Partial<ReadingSettings>, "immediate");
};
return (
<>
{/* Overlay */}
{isOpen && (
<div
className="settings-overlay"
onClick={onClose}
onKeyDown={(e: React.KeyboardEvent) => {
if (e.key === "Escape") onClose();
onClick={handleClose}
onKeyDown={(e: ReactKeyboardEvent) => {
if (e.key === "Escape") handleClose();
}}
role="presentation"
/>
)}
{/* Drawer */}
<aside
className={`settings-drawer ${isOpen ? "settings-drawer--open" : ""}`}
>
<div className="settings-header">
<h2 className="settings-title">Reading Settings</h2>
<h2 className="settings-title">{t("reader.settingsTitle")}</h2>
<button
type="button"
className="settings-close-btn"
onClick={onClose}
aria-label="Close settings"
onClick={handleClose}
aria-label={t("reader.closeSettingsAria")}
>
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="18" y1="6" x2="6" y2="18" />
@@ -91,9 +97,8 @@ export default function ReadingSettingsPanel({
</div>
<div className="settings-body">
{/* Theme Presets */}
<section className="settings-section">
<h3 className="settings-section-title">Theme</h3>
<h3 className="settings-section-title">{t("reader.theme")}</h3>
<div className="theme-grid">
{THEME_OPTIONS.map((opt) => (
<button
@@ -102,18 +107,16 @@ export default function ReadingSettingsPanel({
className={`theme-btn ${
settings.theme === opt.value ? "theme-btn--active" : ""
}`}
onClick={() => handleChange("theme", opt.value)}
disabled={saving.theme}
onClick={() => handleDiscrete("theme", opt.value)}
>
{opt.label}
{t(opt.labelKey)}
</button>
))}
</div>
</section>
{/* Font Family */}
<section className="settings-section">
<h3 className="settings-section-title">Font</h3>
<h3 className="settings-section-title">{t("reader.font")}</h3>
<div className="font-grid">
{FONT_OPTIONS.map((opt) => (
<button
@@ -122,19 +125,17 @@ export default function ReadingSettingsPanel({
className={`font-btn ${
settings.font_family === opt.value ? "font-btn--active" : ""
}`}
onClick={() => handleChange("font_family", opt.value)}
disabled={saving.font_family}
onClick={() => handleDiscrete("font_family", opt.value)}
>
{opt.label}
{t(opt.labelKey)}
</button>
))}
</div>
</section>
{/* Font Size Slider */}
<section className="settings-section">
<h3 className="settings-section-title">
Font Size: {settings.font_size}px
{t("reader.fontSize", { size: String(settings.font_size) })}
</h3>
<input
type="range"
@@ -142,17 +143,16 @@ export default function ReadingSettingsPanel({
max="32"
value={settings.font_size}
onChange={(e) =>
handleChange("font_size", Number(e.target.value))
handleSlider("font_size", Number(e.target.value))
}
className="settings-slider"
aria-label="Font size"
aria-label={t("reader.fontSizeAria")}
/>
</section>
{/* Line Height Slider */}
<section className="settings-section">
<h3 className="settings-section-title">
Line Height: {settings.line_height.toFixed(1)}
{t("reader.lineHeight", { value: settings.line_height.toFixed(1) })}
</h3>
<input
type="range"
@@ -161,17 +161,16 @@ export default function ReadingSettingsPanel({
step="0.1"
value={settings.line_height}
onChange={(e) =>
handleChange("line_height", Number(e.target.value))
handleSlider("line_height", Number(e.target.value))
}
className="settings-slider"
aria-label="Line height"
aria-label={t("reader.lineHeightAria")}
/>
</section>
{/* Margin Width Slider */}
<section className="settings-section">
<h3 className="settings-section-title">
Margins: {settings.margin_width}px
{t("reader.margins", { value: String(settings.margin_width) })}
</h3>
<input
type="range"
@@ -179,17 +178,16 @@ export default function ReadingSettingsPanel({
max="48"
value={settings.margin_width}
onChange={(e) =>
handleChange("margin_width", Number(e.target.value))
handleSlider("margin_width", Number(e.target.value))
}
className="settings-slider"
aria-label="Margin width"
aria-label={t("reader.marginWidthAria")}
/>
</section>
{/* Brightness Slider */}
<section className="settings-section">
<h3 className="settings-section-title">
Brightness: {settings.brightness}%
{t("reader.brightness", { value: String(settings.brightness) })}
</h3>
<input
type="range"
@@ -197,16 +195,15 @@ export default function ReadingSettingsPanel({
max="100"
value={settings.brightness}
onChange={(e) =>
handleChange("brightness", Number(e.target.value))
handleSlider("brightness", Number(e.target.value))
}
className="settings-slider"
aria-label="Brightness"
aria-label={t("reader.brightnessAria")}
/>
</section>
{/* Orientation Lock */}
<section className="settings-section">
<h3 className="settings-section-title">Orientation</h3>
<h3 className="settings-section-title">{t("reader.orientation")}</h3>
<div className="orientation-grid">
{ORIENTATION_OPTIONS.map((opt) => (
<button
@@ -217,10 +214,9 @@ export default function ReadingSettingsPanel({
? "orientation-btn--active"
: ""
}`}
onClick={() => handleChange("orientation_lock", opt.value)}
disabled={saving.orientation_lock}
onClick={() => handleDiscrete("orientation_lock", opt.value)}
>
{opt.label}
{t(opt.labelKey)}
</button>
))}
</div>
@@ -229,4 +225,4 @@ export default function ReadingSettingsPanel({
</aside>
</>
);
}
}