Add database setup script and fix dark mode visibility
- Add scripts/setup-db.ts to programmatically create missing PocketBase collections (period_logs, dailyLogs) with proper relation fields - Fix dark mode visibility across settings, login, calendar, and dashboard components by using semantic CSS tokens and dark: variants - Add db:setup npm script and document usage in AGENTS.md - Update vitest config to include scripts directory tests Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,7 +32,7 @@ export default function RootLayout({
|
||||
>
|
||||
<a
|
||||
href="#main-content"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-white focus:px-4 focus:py-2 focus:rounded focus:shadow-lg focus:text-blue-600 focus:underline"
|
||||
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-50 focus:bg-background focus:px-4 focus:py-2 focus:rounded focus:shadow-lg focus:text-blue-600 dark:focus:text-blue-400 focus:underline focus:border focus:border-input"
|
||||
>
|
||||
Skip to main content
|
||||
</a>
|
||||
|
||||
@@ -200,7 +200,7 @@ export default function LoginPage() {
|
||||
>
|
||||
<div className="w-full max-w-md space-y-8 p-8">
|
||||
<h1 className="text-2xl font-bold text-center">PhaseFlow</h1>
|
||||
<div className="text-center text-gray-500">Loading...</div>
|
||||
<div className="text-center text-muted-foreground">Loading...</div>
|
||||
</div>
|
||||
</main>
|
||||
);
|
||||
@@ -217,7 +217,7 @@ export default function LoginPage() {
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded"
|
||||
className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
@@ -241,7 +241,7 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="email"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Email
|
||||
</label>
|
||||
@@ -251,7 +251,7 @@ export default function LoginPage() {
|
||||
value={email}
|
||||
onChange={(e) => handleInputChange(setEmail, e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-muted disabled:cursor-not-allowed"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
@@ -259,7 +259,7 @@ export default function LoginPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="password"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Password
|
||||
</label>
|
||||
@@ -269,7 +269,7 @@ export default function LoginPage() {
|
||||
value={password}
|
||||
onChange={(e) => handleInputChange(setPassword, e.target.value)}
|
||||
disabled={isLoading}
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-muted disabled:cursor-not-allowed"
|
||||
required
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -156,7 +156,7 @@ export default function GarminSettingsPage() {
|
||||
<h1 className="text-2xl font-bold mb-8">
|
||||
Settings > Garmin Connection
|
||||
</h1>
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -176,28 +176,30 @@ export default function GarminSettingsPage() {
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-6"
|
||||
className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded mb-6"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded mb-6">
|
||||
<div className="bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400 px-4 py-3 rounded mb-6">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-lg space-y-6">
|
||||
{/* Connection Status Section */}
|
||||
<div className="border border-gray-200 rounded-lg p-6">
|
||||
<div className="border border-input rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Connection Status</h2>
|
||||
|
||||
{status?.connected && !status.expired ? (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span className="text-green-700 font-medium">Connected</span>
|
||||
<span className="text-green-700 dark:text-green-400 font-medium">
|
||||
Connected
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{status.warningLevel && (
|
||||
@@ -205,8 +207,8 @@ export default function GarminSettingsPage() {
|
||||
data-testid="expiry-warning"
|
||||
className={`px-4 py-3 rounded ${
|
||||
status.warningLevel === "critical"
|
||||
? "bg-red-50 border border-red-200 text-red-700"
|
||||
: "bg-yellow-50 border border-yellow-200 text-yellow-700"
|
||||
? "bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400"
|
||||
: "bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 text-yellow-700 dark:text-yellow-400"
|
||||
}`}
|
||||
>
|
||||
{status.warningLevel === "critical"
|
||||
@@ -215,7 +217,7 @@ export default function GarminSettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
Token expires in{" "}
|
||||
<span className="font-medium">
|
||||
{status.daysUntilExpiry} days
|
||||
@@ -235,33 +237,35 @@ export default function GarminSettingsPage() {
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="w-3 h-3 bg-red-500 rounded-full" />
|
||||
<span className="text-red-700 font-medium">Token Expired</span>
|
||||
<span className="text-red-700 dark:text-red-400 font-medium">
|
||||
Token Expired
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-gray-600">
|
||||
<p className="text-muted-foreground">
|
||||
Your Garmin tokens have expired. Please generate new tokens and
|
||||
paste them below.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-2">
|
||||
<span className="w-3 h-3 bg-gray-400 rounded-full" />
|
||||
<span className="text-gray-600">Not Connected</span>
|
||||
<span className="w-3 h-3 bg-muted-foreground rounded-full" />
|
||||
<span className="text-muted-foreground">Not Connected</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Token Input Section */}
|
||||
{showTokenInput && (
|
||||
<div className="border border-gray-200 rounded-lg p-6">
|
||||
<div className="border border-input rounded-lg p-6">
|
||||
<h2 className="text-lg font-semibold mb-4">Connect Garmin</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 text-blue-700 px-4 py-3 rounded text-sm">
|
||||
<div className="bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 text-blue-700 dark:text-blue-400 px-4 py-3 rounded text-sm">
|
||||
<p className="font-medium mb-2">Instructions:</p>
|
||||
<ol className="list-decimal list-inside space-y-1">
|
||||
<li>
|
||||
Run{" "}
|
||||
<code className="bg-blue-100 px-1 rounded">
|
||||
<code className="bg-blue-100 dark:bg-blue-900 px-1 rounded">
|
||||
python3 scripts/garmin_auth.py
|
||||
</code>{" "}
|
||||
locally
|
||||
@@ -274,7 +278,7 @@ export default function GarminSettingsPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="tokenInput"
|
||||
className="block text-sm font-medium text-gray-700 mb-1"
|
||||
className="block text-sm font-medium text-foreground mb-1"
|
||||
>
|
||||
Paste Tokens (JSON)
|
||||
</label>
|
||||
@@ -285,7 +289,7 @@ export default function GarminSettingsPage() {
|
||||
onChange={(e) => handleTokenChange(e.target.value)}
|
||||
disabled={saving}
|
||||
placeholder='{"oauth1": {...}, "oauth2": {...}, "expires_at": "..."}'
|
||||
className="block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed font-mono text-sm"
|
||||
className="block w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-muted disabled:cursor-not-allowed font-mono text-sm"
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -132,7 +132,7 @@ export default function SettingsPage() {
|
||||
return (
|
||||
<main id="main-content" className="container mx-auto p-8">
|
||||
<h1 className="text-2xl font-bold mb-8">Settings</h1>
|
||||
<p className="text-gray-500">Loading...</p>
|
||||
<p className="text-muted-foreground">Loading...</p>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
@@ -152,31 +152,33 @@ export default function SettingsPage() {
|
||||
{error && (
|
||||
<div
|
||||
role="alert"
|
||||
className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded mb-6"
|
||||
className="bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded mb-6"
|
||||
>
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-50 border border-green-200 text-green-700 px-4 py-3 rounded mb-6">
|
||||
<div className="bg-green-50 dark:bg-green-950 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400 px-4 py-3 rounded mb-6">
|
||||
{success}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="max-w-lg">
|
||||
<div className="mb-6">
|
||||
<span className="block text-sm font-medium text-gray-700">Email</span>
|
||||
<p className="mt-1 text-gray-900">{userData?.email}</p>
|
||||
<span className="block text-sm font-medium text-foreground">
|
||||
Email
|
||||
</span>
|
||||
<p className="mt-1 text-foreground">{userData?.email}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-6 p-4 border border-gray-200 rounded-lg">
|
||||
<div className="mb-6 p-4 border border-input rounded-lg">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<span className="block text-sm font-medium text-gray-700">
|
||||
<span className="block text-sm font-medium text-foreground">
|
||||
Garmin Connection
|
||||
</span>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{userData?.garminConnected
|
||||
? "Connected to Garmin"
|
||||
: "Not connected"}
|
||||
@@ -195,7 +197,7 @@ export default function SettingsPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="cycleLength"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Cycle Length (days)
|
||||
</label>
|
||||
@@ -209,10 +211,10 @@ export default function SettingsPage() {
|
||||
handleInputChange(setCycleLength, Number(e.target.value))
|
||||
}
|
||||
disabled={saving}
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-muted disabled:cursor-not-allowed"
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Typical range: 21-45 days
|
||||
</p>
|
||||
</div>
|
||||
@@ -220,7 +222,7 @@ export default function SettingsPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="notificationTime"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Notification Time
|
||||
</label>
|
||||
@@ -232,10 +234,10 @@ export default function SettingsPage() {
|
||||
handleInputChange(setNotificationTime, e.target.value)
|
||||
}
|
||||
disabled={saving}
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-muted disabled:cursor-not-allowed [color-scheme:light] dark:[color-scheme:dark]"
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
Time to receive daily email notification
|
||||
</p>
|
||||
</div>
|
||||
@@ -243,7 +245,7 @@ export default function SettingsPage() {
|
||||
<div>
|
||||
<label
|
||||
htmlFor="timezone"
|
||||
className="block text-sm font-medium text-gray-700"
|
||||
className="block text-sm font-medium text-foreground"
|
||||
>
|
||||
Timezone
|
||||
</label>
|
||||
@@ -253,11 +255,11 @@ export default function SettingsPage() {
|
||||
value={timezone}
|
||||
onChange={(e) => handleInputChange(setTimezone, e.target.value)}
|
||||
disabled={saving}
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-gray-100 disabled:cursor-not-allowed"
|
||||
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 shadow-sm focus:border-blue-500 focus:outline-none focus:ring-1 focus:ring-blue-500 disabled:bg-muted disabled:cursor-not-allowed"
|
||||
placeholder="America/New_York"
|
||||
required
|
||||
/>
|
||||
<p className="mt-1 text-sm text-gray-500">
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
IANA timezone (e.g., America/New_York, Europe/London)
|
||||
</p>
|
||||
</div>
|
||||
@@ -273,8 +275,8 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<div className="mt-8 pt-8 border-t border-gray-200">
|
||||
<h2 className="text-lg font-medium text-gray-900 mb-4">Account</h2>
|
||||
<div className="mt-8 pt-8 border-t border-input">
|
||||
<h2 className="text-lg font-medium text-foreground mb-4">Account</h2>
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleLogout}
|
||||
|
||||
@@ -12,21 +12,27 @@ function getStatusColors(status: Decision["status"]): {
|
||||
} {
|
||||
switch (status) {
|
||||
case "REST":
|
||||
return { background: "bg-red-100 border-red-300", text: "text-red-700" };
|
||||
return {
|
||||
background:
|
||||
"bg-red-100 dark:bg-red-900/50 border-red-300 dark:border-red-700",
|
||||
text: "text-red-700 dark:text-red-300",
|
||||
};
|
||||
case "GENTLE":
|
||||
case "LIGHT":
|
||||
case "REDUCED":
|
||||
return {
|
||||
background: "bg-yellow-100 border-yellow-300",
|
||||
text: "text-yellow-700",
|
||||
background:
|
||||
"bg-yellow-100 dark:bg-yellow-900/50 border-yellow-300 dark:border-yellow-700",
|
||||
text: "text-yellow-700 dark:text-yellow-300",
|
||||
};
|
||||
case "TRAIN":
|
||||
return {
|
||||
background: "bg-green-100 border-green-300",
|
||||
text: "text-green-700",
|
||||
background:
|
||||
"bg-green-100 dark:bg-green-900/50 border-green-300 dark:border-green-700",
|
||||
text: "text-green-700 dark:text-green-300",
|
||||
};
|
||||
default:
|
||||
return { background: "border", text: "text-gray-600" };
|
||||
return { background: "border", text: "text-muted-foreground" };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,21 +14,24 @@ interface MiniCalendarProps {
|
||||
}
|
||||
|
||||
const PHASE_COLORS: Record<CyclePhase, string> = {
|
||||
MENSTRUAL: "bg-blue-100",
|
||||
FOLLICULAR: "bg-green-100",
|
||||
OVULATION: "bg-purple-100",
|
||||
EARLY_LUTEAL: "bg-yellow-100",
|
||||
LATE_LUTEAL: "bg-red-100",
|
||||
MENSTRUAL: "bg-blue-100 dark:bg-blue-900/50 text-blue-900 dark:text-blue-100",
|
||||
FOLLICULAR:
|
||||
"bg-green-100 dark:bg-green-900/50 text-green-900 dark:text-green-100",
|
||||
OVULATION:
|
||||
"bg-purple-100 dark:bg-purple-900/50 text-purple-900 dark:text-purple-100",
|
||||
EARLY_LUTEAL:
|
||||
"bg-yellow-100 dark:bg-yellow-900/50 text-yellow-900 dark:text-yellow-100",
|
||||
LATE_LUTEAL: "bg-red-100 dark:bg-red-900/50 text-red-900 dark:text-red-100",
|
||||
};
|
||||
|
||||
const COMPACT_DAY_NAMES = ["S", "M", "T", "W", "T", "F", "S"];
|
||||
|
||||
const PHASE_LEGEND = [
|
||||
{ name: "Menstrual", color: "bg-blue-100" },
|
||||
{ name: "Follicular", color: "bg-green-100" },
|
||||
{ name: "Ovulation", color: "bg-purple-100" },
|
||||
{ name: "Early Luteal", color: "bg-yellow-100" },
|
||||
{ name: "Late Luteal", color: "bg-red-100" },
|
||||
{ name: "Menstrual", color: "bg-blue-100 dark:bg-blue-900/50" },
|
||||
{ name: "Follicular", color: "bg-green-100 dark:bg-green-900/50" },
|
||||
{ name: "Ovulation", color: "bg-purple-100 dark:bg-purple-900/50" },
|
||||
{ name: "Early Luteal", color: "bg-yellow-100 dark:bg-yellow-900/50" },
|
||||
{ name: "Late Luteal", color: "bg-red-100 dark:bg-red-900/50" },
|
||||
];
|
||||
|
||||
function getDaysInMonth(year: number, month: number): number {
|
||||
@@ -102,7 +105,7 @@ export function MiniCalendar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handlePreviousMonth}
|
||||
className="p-1 hover:bg-gray-100 rounded text-sm"
|
||||
className="p-1 hover:bg-muted rounded text-sm"
|
||||
aria-label="Previous month"
|
||||
>
|
||||
←
|
||||
@@ -117,7 +120,7 @@ export function MiniCalendar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleTodayClick}
|
||||
className="px-2 py-0.5 text-xs border rounded hover:bg-gray-100"
|
||||
className="px-2 py-0.5 text-xs border rounded hover:bg-muted"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
@@ -125,7 +128,7 @@ export function MiniCalendar({
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleNextMonth}
|
||||
className="p-1 hover:bg-gray-100 rounded text-sm"
|
||||
className="p-1 hover:bg-muted rounded text-sm"
|
||||
aria-label="Next month"
|
||||
>
|
||||
→
|
||||
@@ -138,7 +141,7 @@ export function MiniCalendar({
|
||||
<div
|
||||
// biome-ignore lint/suspicious/noArrayIndexKey: Day names are fixed and index is stable
|
||||
key={`day-header-${index}`}
|
||||
className="text-center text-xs font-medium text-gray-500"
|
||||
className="text-center text-xs font-medium text-muted-foreground"
|
||||
>
|
||||
{dayName}
|
||||
</div>
|
||||
@@ -165,7 +168,7 @@ export function MiniCalendar({
|
||||
type="button"
|
||||
key={date.toISOString()}
|
||||
className={`p-1 text-xs rounded ${PHASE_COLORS[phase]} ${
|
||||
isToday ? "ring-2 ring-black font-bold" : ""
|
||||
isToday ? "ring-2 ring-foreground font-bold" : ""
|
||||
}`}
|
||||
>
|
||||
{date.getDate()}
|
||||
@@ -182,7 +185,7 @@ export function MiniCalendar({
|
||||
{PHASE_LEGEND.map((phase) => (
|
||||
<div key={phase.name} className="flex items-center gap-0.5">
|
||||
<div className={`w-2 h-2 rounded ${phase.color}`} />
|
||||
<span className="text-xs text-gray-600">{phase.name}</span>
|
||||
<span className="text-xs text-muted-foreground">{phase.name}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user