Add multiple entries per day, user management, reminders, and AI reflections

- Multiple entries per day: Home page now starts fresh, Save & New button
- Admin user management: Add/delete users, reset passwords, toggle admin
- Daily reminders: Browser notifications at configurable time
- AI reflections: Generate insights from entries using Claude API
- Remove cloud sync placeholder (already have user accounts)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Gemini Agent
2026-01-24 12:05:39 +00:00
parent bd6703b24b
commit 504f07a106
21 changed files with 2104 additions and 96 deletions

View File

@@ -1,12 +1,133 @@
"use client";
import { useState, useEffect } from "react";
import { AppShell } from "@/components/AppShell";
import { useAuth } from "@/components/AuthProvider";
import { APP_NAME } from "@/lib/constants";
import { LogOut } from "lucide-react";
import { LogOut, Users, Bell, Sparkles, Loader2, Check, Key } from "lucide-react";
import Link from "next/link";
interface UserSettings {
reminderEnabled: boolean;
reminderTime: string;
hasLlmKey: boolean;
}
export default function SettingsPage() {
const { user, logout } = useAuth();
const [settings, setSettings] = useState<UserSettings | null>(null);
const [isLoading, setIsLoading] = useState(true);
const [isSaving, setIsSaving] = useState(false);
const [showApiKeyInput, setShowApiKeyInput] = useState(false);
const [apiKey, setApiKey] = useState("");
const [notificationPermission, setNotificationPermission] = useState<NotificationPermission>("default");
useEffect(() => {
async function loadSettings() {
try {
const res = await fetch("/api/settings");
if (res.ok) {
setSettings(await res.json());
}
} catch (error) {
console.error("Failed to load settings:", error);
} finally {
setIsLoading(false);
}
}
loadSettings();
// Check notification permission
if ("Notification" in window) {
setNotificationPermission(Notification.permission);
}
}, []);
const updateSetting = async (key: string, value: unknown) => {
setIsSaving(true);
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ [key]: value }),
});
if (res.ok) {
setSettings((prev) => prev ? { ...prev, [key]: value } : null);
}
} catch (error) {
console.error("Failed to update setting:", error);
} finally {
setIsSaving(false);
}
};
const requestNotificationPermission = async () => {
if ("Notification" in window) {
const permission = await Notification.requestPermission();
setNotificationPermission(permission);
if (permission === "granted") {
updateSetting("reminderEnabled", true);
}
}
};
const toggleReminder = async () => {
if (!settings) return;
if (!settings.reminderEnabled) {
// Enabling - need permission first
if (notificationPermission !== "granted") {
await requestNotificationPermission();
} else {
updateSetting("reminderEnabled", true);
}
} else {
updateSetting("reminderEnabled", false);
}
};
const saveApiKey = async () => {
setIsSaving(true);
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ llmApiKey: apiKey }),
});
if (res.ok) {
setSettings((prev) => prev ? { ...prev, hasLlmKey: Boolean(apiKey) } : null);
setShowApiKeyInput(false);
setApiKey("");
}
} catch (error) {
console.error("Failed to save API key:", error);
} finally {
setIsSaving(false);
}
};
// Set up reminder check interval
useEffect(() => {
if (!settings?.reminderEnabled || !settings?.reminderTime) return;
const checkReminder = () => {
const now = new Date();
const [hours, minutes] = settings.reminderTime.split(":").map(Number);
if (now.getHours() === hours && now.getMinutes() === minutes) {
if (Notification.permission === "granted") {
new Notification(APP_NAME, {
body: "Take a moment to reflect on what you're grateful for today.",
icon: "/icon.png",
});
}
}
};
// Check every minute
const interval = setInterval(checkReminder, 60000);
return () => clearInterval(interval);
}, [settings?.reminderEnabled, settings?.reminderTime]);
return (
<AppShell>
@@ -32,59 +153,161 @@ export default function SettingsPage() {
</div>
)}
{/* Notifications - disabled in MVP */}
<div className="p-4 bg-surface border border-border rounded-xl opacity-50">
<div className="flex items-center justify-between">
{/* Admin - only show for admin users */}
{user?.isAdmin && (
<Link
href="/admin"
className="p-4 bg-surface border border-border rounded-xl flex items-center gap-3 hover:border-muted transition-colors"
>
<Users size={20} className="text-accent" />
<div>
<h3 className="font-medium">Daily reminder</h3>
<p className="text-sm text-muted">
Get a gentle nudge to check in
</p>
<h3 className="font-medium">User Management</h3>
<p className="text-sm text-muted">Add users, reset passwords</p>
</div>
<button
disabled
className="relative w-12 h-6 bg-border rounded-full cursor-not-allowed"
>
<span className="absolute left-1 top-1 w-4 h-4 bg-muted rounded-full" />
</button>
</div>
</div>
</Link>
)}
{/* Sync - disabled in MVP */}
<div className="p-4 bg-surface border border-border rounded-xl opacity-50">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">Cloud sync</h3>
<p className="text-sm text-muted">
Sync across devices (coming soon)
</p>
{/* Daily Reminder */}
{!isLoading && settings && (
<div className="p-4 bg-surface border border-border rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Bell size={20} className="text-accent" />
<div>
<h3 className="font-medium">Daily reminder</h3>
<p className="text-sm text-muted">
Get a gentle nudge to check in
</p>
</div>
</div>
<button
onClick={toggleReminder}
disabled={isSaving}
className={`relative w-12 h-6 rounded-full transition-colors ${
settings.reminderEnabled ? "bg-accent" : "bg-border"
}`}
>
<span
className={`absolute top-1 w-4 h-4 bg-white rounded-full transition-transform ${
settings.reminderEnabled ? "left-7" : "left-1"
}`}
/>
</button>
</div>
<button
disabled
className="relative w-12 h-6 bg-border rounded-full cursor-not-allowed"
>
<span className="absolute left-1 top-1 w-4 h-4 bg-muted rounded-full" />
</button>
{settings.reminderEnabled && (
<div className="mt-4 flex items-center gap-3">
<label className="text-sm text-muted">Remind me at:</label>
<input
type="time"
value={settings.reminderTime}
onChange={(e) => updateSetting("reminderTime", e.target.value)}
className="px-3 py-1 bg-background border border-border rounded-lg text-sm focus:outline-none focus:border-muted"
/>
</div>
)}
{notificationPermission === "denied" && (
<p className="mt-2 text-sm text-red-400">
Notifications are blocked. Please enable them in your browser settings.
</p>
)}
</div>
</div>
)}
{/* LLM Summaries - disabled in MVP */}
<div className="p-4 bg-surface border border-border rounded-xl opacity-50">
<div className="flex items-center justify-between">
<div>
<h3 className="font-medium">AI summaries</h3>
<p className="text-sm text-muted">
Monthly reflections with LLM (coming soon)
</p>
{/* AI Reflections */}
{!isLoading && settings && (
<div className="p-4 bg-surface border border-border rounded-xl">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Sparkles size={20} className="text-accent" />
<div>
<h3 className="font-medium">AI reflections</h3>
<p className="text-sm text-muted">
Generate insights from your entries
</p>
</div>
</div>
{settings.hasLlmKey ? (
<Check size={20} className="text-green-500" />
) : (
<button
onClick={() => setShowApiKeyInput(true)}
className="px-3 py-1 text-sm bg-accent text-white rounded-lg hover:bg-accent/90"
>
Set up
</button>
)}
</div>
<button
disabled
className="relative w-12 h-6 bg-border rounded-full cursor-not-allowed"
>
<span className="absolute left-1 top-1 w-4 h-4 bg-muted rounded-full" />
</button>
{settings.hasLlmKey && !showApiKeyInput && (
<div className="mt-3 flex items-center gap-2">
<span className="text-sm text-green-500">API key configured</span>
<button
onClick={() => setShowApiKeyInput(true)}
className="text-sm text-muted hover:text-foreground"
>
Change
</button>
<span className="text-muted"></span>
<Link
href="/reflections"
className="text-sm text-accent hover:underline"
>
View reflections
</Link>
</div>
)}
{showApiKeyInput && (
<div className="mt-4 space-y-3">
<div>
<label className="block text-sm text-muted mb-1">
Claude API Key
</label>
<div className="flex gap-2">
<div className="relative flex-1">
<Key size={16} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted" />
<input
type="password"
value={apiKey}
onChange={(e) => setApiKey(e.target.value)}
placeholder="sk-ant-..."
className="w-full pl-9 pr-3 py-2 bg-background border border-border rounded-lg text-sm focus:outline-none focus:border-muted"
/>
</div>
<button
onClick={saveApiKey}
disabled={isSaving || !apiKey}
className="px-4 py-2 bg-accent text-white rounded-lg hover:bg-accent/90 disabled:opacity-50 flex items-center gap-2"
>
{isSaving ? <Loader2 size={14} className="animate-spin" /> : null}
Save
</button>
</div>
</div>
<p className="text-xs text-muted">
Get your API key from{" "}
<a
href="https://console.anthropic.com/settings/keys"
target="_blank"
rel="noopener noreferrer"
className="text-accent hover:underline"
>
console.anthropic.com
</a>
</p>
<button
onClick={() => {
setShowApiKeyInput(false);
setApiKey("");
}}
className="text-sm text-muted hover:text-foreground"
>
Cancel
</button>
</div>
)}
</div>
</div>
)}
<div className="pt-6 border-t border-border text-center text-sm text-muted">
<p>{APP_NAME} v0.1.0</p>