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:
@@ -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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user