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
+91 -27
View File
@@ -1,15 +1,25 @@
/**
* useReadingSettings — fetch and manage user reading preferences.
* Applies settings as CSS custom properties on the document root.
* Preview applies instantly to the reader; API saves are debounced for sliders.
*/
import { useCallback, useEffect, useState } from "react";
import { useCallback, useEffect, useRef, useState } from "react";
import {
getReadingSettings,
updateReadingSettings,
} from "../api/reader";
import type { ReadingSettings } from "../types/reader";
const THEME_COLORS: Record<
ReadingSettings["theme"],
Pick<ReadingSettings, "background_color" | "text_color">
> = {
sepia: { background_color: "#f5f0eb", text_color: "#1a1a1a" },
dark: { background_color: "#1a1a2e", text_color: "#e0e0e0" },
light: { background_color: "#ffffff", text_color: "#1a1a1a" },
paper: { background_color: "#e8e0d4", text_color: "#2c2c2c" },
};
const DEFAULT_SETTINGS: ReadingSettings = {
font_family: "serif",
font_size: 18,
@@ -24,22 +34,38 @@ const DEFAULT_SETTINGS: ReadingSettings = {
updated_at: "",
};
function applyCssVariables(settings: ReadingSettings): void {
const SAVE_DEBOUNCE_MS = 600;
export function applyReadingCssVariables(settings: ReadingSettings): void {
const root = document.documentElement;
root.style.setProperty("--reader-bg", settings.background_color);
root.style.setProperty("--reader-text", settings.text_color);
root.style.setProperty("--reader-font-family", settings.font_family);
root.style.setProperty("--reader-font-size", `${settings.font_size}px`);
root.style.setProperty("--reader-line-height", String(settings.line_height));
root.style.setProperty("--reader-margin", `${settings.margin_width}px`);
root.style.setProperty("--reader-brightness", `${settings.brightness}%`);
}
function mergeSettings(
prev: ReadingSettings,
partial: Partial<ReadingSettings>,
): ReadingSettings {
const next = { ...prev, ...partial };
if (partial.theme && THEME_COLORS[partial.theme]) {
Object.assign(next, THEME_COLORS[partial.theme]);
}
return next;
}
export type SettingsPersistMode = "debounced" | "immediate";
export interface UseReadingSettingsReturn {
settings: ReadingSettings;
isLoading: boolean;
error: string | null;
updateSettings: (partial: Partial<ReadingSettings>) => Promise<void>;
updateSettings: (
partial: Partial<ReadingSettings>,
persist?: SettingsPersistMode,
) => void;
flushSettings: () => Promise<void>;
}
export function useReadingSettings(): UseReadingSettingsReturn {
@@ -47,7 +73,45 @@ export function useReadingSettings(): UseReadingSettingsReturn {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
// Fetch settings on mount
const pendingPatch = useRef<Partial<ReadingSettings>>({});
const saveTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
const saving = useRef(false);
const runPersist = useCallback(async () => {
const payload = { ...pendingPatch.current };
pendingPatch.current = {};
if (Object.keys(payload).length === 0 || saving.current) return;
saving.current = true;
try {
const updated = await updateReadingSettings(payload);
setSettings(updated);
applyReadingCssVariables(updated);
setError(null);
} catch (err: unknown) {
setError(
err instanceof Error ? err.message : "Failed to update reading settings",
);
} finally {
saving.current = false;
}
}, []);
const schedulePersist = useCallback(
(partial: Partial<ReadingSettings>, immediate: boolean) => {
Object.assign(pendingPatch.current, partial);
if (saveTimer.current) clearTimeout(saveTimer.current);
if (immediate) {
void runPersist();
return;
}
saveTimer.current = setTimeout(() => {
void runPersist();
}, SAVE_DEBOUNCE_MS);
},
[runPersist],
);
useEffect(() => {
let cancelled = false;
setIsLoading(true);
@@ -55,16 +119,15 @@ export function useReadingSettings(): UseReadingSettingsReturn {
.then((data) => {
if (!cancelled) {
setSettings(data);
applyCssVariables(data);
applyReadingCssVariables(data);
}
})
.catch((err: unknown) => {
if (!cancelled) {
setError(
err instanceof Error ? err.message : "Failed to load reading settings"
err instanceof Error ? err.message : "Failed to load reading settings",
);
// Apply defaults
applyCssVariables(DEFAULT_SETTINGS);
applyReadingCssVariables(DEFAULT_SETTINGS);
}
})
.finally(() => {
@@ -72,25 +135,26 @@ export function useReadingSettings(): UseReadingSettingsReturn {
});
return () => {
cancelled = true;
if (saveTimer.current) clearTimeout(saveTimer.current);
};
}, []);
const updateSettings = useCallback(
async (partial: Partial<ReadingSettings>) => {
try {
const updated = await updateReadingSettings(partial);
setSettings(updated);
applyCssVariables(updated);
setError(null);
} catch (err: unknown) {
setError(
err instanceof Error ? err.message : "Failed to update reading settings"
);
throw err;
}
(partial: Partial<ReadingSettings>, persist: SettingsPersistMode = "debounced") => {
setSettings((prev) => {
const next = mergeSettings(prev, partial);
applyReadingCssVariables(next);
return next;
});
schedulePersist(partial, persist === "immediate");
},
[]
[schedulePersist],
);
return { settings, isLoading, error, updateSettings };
}
const flushSettings = useCallback(async () => {
if (saveTimer.current) clearTimeout(saveTimer.current);
await runPersist();
}, [runPersist]);
return { settings, isLoading, error, updateSettings, flushSettings };
}