added dark mode

This commit is contained in:
2026-02-15 23:11:33 -06:00
parent e97347ff65
commit d8fde5b516
18 changed files with 543 additions and 366 deletions

View File

@@ -0,0 +1,106 @@
# Dark Mode Implementation Plan
## Context
The Synculous client already has a bare `@media (prefers-color-scheme: dark)` in `globals.css` that only flips `body` background/foreground. Every component uses hardcoded Tailwind classes (`bg-white`, `bg-gray-50`, `text-gray-900`, etc.) with no `dark:` variants, so manually switching modes via a button has no effect. The goal is a user-controlled toggle that persists preference, respects the OS default, and applies comprehensive dark styles across all pages.
---
## Steps
### 1. Configure Tailwind v4 for class-based dark mode
**File:** `src/app/globals.css`
Add the Tailwind v4 CSS directive that maps `dark:` utilities to an ancestor `.dark` class:
```css
@variant dark (&:where(.dark, .dark *));
```
Replace the `@media (prefers-color-scheme: dark)` block with a `.dark` selector:
```css
.dark {
--background: #0a0a0a;
--foreground: #ededed;
}
```
### 2. Create ThemeProvider
**New file:** `src/components/theme/ThemeProvider.tsx`
A minimal React context provider:
- State: `isDark: boolean`
- On mount: reads `localStorage.getItem('theme')`, falls back to `window.matchMedia('(prefers-color-scheme: dark)').matches`
- Applies/removes `dark` class on `document.documentElement`
- `toggleDark()` function that flips state and saves to `localStorage`
- Exports `useTheme()` hook
### 3. Anti-flash script + wrap in layout
**File:** `src/app/layout.tsx`
- Add ThemeProvider wrapping `AuthProvider`
- Add inline `<script>` before `<body>` content to set `.dark` class before hydration (prevents white flash)
### 4. Dark mode toggle button in dashboard header
**File:** `src/app/dashboard/layout.tsx`
- Import `useTheme` from the ThemeProvider
- Add a sun/moon icon button in the existing right-side icon row (between Settings and Logout)
- `SunIcon` and `MoonIcon` already exist in `src/components/ui/Icons.tsx` — no new icons needed
### 5. Add `dark:` variants to all components
Color mapping:
| Light class | Dark variant |
|---|---|
| `bg-white` | `dark:bg-gray-800` |
| `bg-gray-50` | `dark:bg-gray-900` |
| `bg-gray-100` | `dark:bg-gray-700` |
| `bg-gray-200` | `dark:bg-gray-600` |
| `text-gray-900` | `dark:text-gray-100` |
| `text-gray-700` | `dark:text-gray-300` |
| `text-gray-600` | `dark:text-gray-400` |
| `text-gray-500` | `dark:text-gray-400` |
| `border-gray-200` | `dark:border-gray-700` |
| `border-gray-100` | `dark:border-gray-800` |
| `border-gray-300` | `dark:border-gray-600` |
| `bg-indigo-50` | `dark:bg-indigo-900/30` |
| `bg-green-50` | `dark:bg-green-900/30` |
| `bg-amber-50` | `dark:bg-amber-900/30` |
| `bg-blue-50` | `dark:bg-blue-900/30` |
| `bg-red-50` | `dark:bg-red-900/30` |
**Files to update (in order):**
1. `src/app/dashboard/layout.tsx` — shell, header, nav
2. `src/app/dashboard/page.tsx` — Today view
3. `src/app/dashboard/routines/page.tsx`
4. `src/app/dashboard/routines/new/page.tsx`
5. `src/app/dashboard/routines/[id]/page.tsx`
6. `src/app/dashboard/routines/[id]/launch/page.tsx`
7. `src/app/dashboard/routines/[id]/run/page.tsx`
8. `src/app/dashboard/templates/page.tsx`
9. `src/app/dashboard/history/page.tsx`
10. `src/app/dashboard/stats/page.tsx`
11. `src/app/dashboard/medications/page.tsx`
12. `src/app/dashboard/medications/new/page.tsx`
13. `src/app/dashboard/settings/page.tsx`
14. `src/app/login/page.tsx`
15. `src/components/session/VisualTimeline.tsx`
16. `src/components/notifications/PushNotificationToggle.tsx`
---
## Files to create
- `src/components/theme/ThemeProvider.tsx` (new)
## Files to modify
- `src/app/globals.css`
- `src/app/layout.tsx`
- All pages listed in step 5
---
## Verification
1. Run `npm run dev` in `synculous-client/`
2. Navigate to `/dashboard` — should default to OS preference
3. Click the sun/moon toggle in the header — UI should switch immediately
4. Refresh the page — theme should persist (no flash)
5. Switch OS theme — manual override takes priority; no override follows OS

View File

@@ -95,13 +95,13 @@ export default function HistoryPage() {
return (
<div className="p-4 space-y-4">
<h1 className="text-2xl font-bold text-gray-900">History</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">History</h1>
{/* Filter */}
<select
value={selectedRoutine}
onChange={(e) => setSelectedRoutine(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl bg-white"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100"
>
<option value="all">All Routines</option>
{routines.map((routine) => (
@@ -112,39 +112,39 @@ export default function HistoryPage() {
</select>
{history.length === 0 ? (
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
<CalendarIcon className="text-gray-400 mx-auto mb-4" size={40} />
<h3 className="font-semibold text-gray-900 mb-1">No history yet</h3>
<p className="text-gray-500 text-sm">Complete a routine to see it here</p>
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
<CalendarIcon className="text-gray-400 dark:text-gray-500 mx-auto mb-4" size={40} />
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No history yet</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm">Complete a routine to see it here</p>
</div>
) : (
<div className="space-y-3">
{history.map((session) => (
<div
key={session.id}
className="bg-white rounded-xl p-4 shadow-sm flex items-center gap-4"
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm flex items-center gap-4"
>
<div className={`
w-10 h-10 rounded-full flex items-center justify-center
${session.status === 'completed' ? 'bg-green-100' : 'bg-red-100'}
${session.status === 'completed' ? 'bg-green-100 dark:bg-green-900/30' : 'bg-red-100 dark:bg-red-900/30'}
`}>
{session.status === 'completed' ? (
<CheckIcon className="text-green-600" size={20} />
<CheckIcon className="text-green-600 dark:text-green-400" size={20} />
) : (
<XIcon className="text-red-600" size={20} />
<XIcon className="text-red-600 dark:text-red-400" size={20} />
)}
</div>
<div className="flex-1">
<p className="font-medium text-gray-900">
<p className="font-medium text-gray-900 dark:text-gray-100">
{(session as any).routine_name || 'Routine'}
</p>
<p className="text-sm text-gray-500">
<p className="text-sm text-gray-500 dark:text-gray-400">
{formatDate(session.created_at)}
</p>
</div>
<span className={`
text-xs font-medium px-2 py-1 rounded-full
${session.status === 'completed' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}
${session.status === 'completed' ? 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400' : 'bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400'}
`}>
{session.status}
</span>

View File

@@ -3,17 +3,20 @@
import { useEffect, useRef } from 'react';
import { useRouter, usePathname } from 'next/navigation';
import { useAuth } from '@/components/auth/AuthProvider';
import { useTheme } from '@/components/theme/ThemeProvider';
import api from '@/lib/api';
import {
HomeIcon,
ListIcon,
CalendarIcon,
BarChartIcon,
import {
HomeIcon,
ListIcon,
CalendarIcon,
BarChartIcon,
PillIcon,
SettingsIcon,
LogOutIcon,
CopyIcon,
HeartIcon
HeartIcon,
SunIcon,
MoonIcon,
} from '@/components/ui/Icons';
import Link from 'next/link';
@@ -32,6 +35,7 @@ export default function DashboardLayout({
children: React.ReactNode;
}) {
const { isAuthenticated, isLoading, logout } = useAuth();
const { isDark, toggleDark } = useTheme();
const router = useRouter();
const pathname = usePathname();
@@ -57,7 +61,7 @@ export default function DashboardLayout({
<div className="min-h-screen flex items-center justify-center">
<div className="animate-pulse flex flex-col items-center">
<div className="w-12 h-12 border-4 border-indigo-500 border-t-transparent rounded-full animate-spin"></div>
<p className="mt-4 text-gray-600">Loading...</p>
<p className="mt-4 text-gray-600 dark:text-gray-400">Loading...</p>
</div>
</div>
);
@@ -75,22 +79,29 @@ export default function DashboardLayout({
}
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-2">
<div className="w-8 h-8 bg-gradient-to-br from-indigo-500 to-pink-500 rounded-lg flex items-center justify-center">
<HeartIcon className="text-white" size={16} />
</div>
<span className="font-bold text-gray-900">Synculous</span>
<span className="font-bold text-gray-900 dark:text-gray-100">Synculous</span>
</div>
<div className="flex items-center gap-2">
<Link href="/dashboard/settings" className="p-2 text-gray-500 hover:text-gray-700">
<button
onClick={toggleDark}
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
aria-label="Toggle dark mode"
>
{isDark ? <SunIcon size={20} /> : <MoonIcon size={20} />}
</button>
<Link href="/dashboard/settings" className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
<SettingsIcon size={20} />
</Link>
<button
onClick={logout}
className="p-2 text-gray-500 hover:text-gray-700"
className="p-2 text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
>
<LogOutIcon size={20} />
</button>
@@ -102,7 +113,7 @@ export default function DashboardLayout({
{children}
</main>
<nav className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-200 safe-area-bottom">
<nav className="fixed bottom-0 left-0 right-0 bg-white dark:bg-gray-900 border-t border-gray-200 dark:border-gray-700 safe-area-bottom">
<div className="flex justify-around py-2">
{navItems.map((item) => {
const isActive = pathname === item.href ||
@@ -113,8 +124,8 @@ export default function DashboardLayout({
href={item.href}
className={`flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-colors ${
isActive
? 'text-indigo-600'
: 'text-gray-500 hover:text-gray-700'
? 'text-indigo-600 dark:text-indigo-400'
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
}`}
>
<item.icon size={20} />

View File

@@ -81,52 +81,52 @@ export default function NewMedicationPage() {
};
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="flex items-center gap-3 px-4 py-3">
<button onClick={() => router.back()} className="p-1">
<button onClick={() => router.back()} className="p-1 text-gray-600 dark:text-gray-400">
<ArrowLeftIcon size={24} />
</button>
<h1 className="text-xl font-bold text-gray-900">Add Medication</h1>
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">Add Medication</h1>
</div>
</header>
<form onSubmit={handleSubmit} className="p-4 space-y-6">
{error && (
<div className="bg-red-50 text-red-600 px-4 py-3 rounded-lg text-sm">
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<div className="bg-white rounded-xl p-4 shadow-sm space-y-4">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Medication Name</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Medication Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="e.g., Vitamin D"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Dosage</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Dosage</label>
<input
type="text"
value={dosage}
onChange={(e) => setDosage(e.target.value)}
placeholder="e.g., 1000"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Unit</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Unit</label>
<select
value={unit}
onChange={(e) => setUnit(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
>
<option value="mg">mg</option>
<option value="mcg">mcg</option>
@@ -140,11 +140,11 @@ export default function NewMedicationPage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Frequency</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Frequency</label>
<select
value={frequency}
onChange={(e) => setFrequency(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
>
<option value="daily">Daily</option>
<option value="specific_days">Specific Days of Week</option>
@@ -156,7 +156,7 @@ export default function NewMedicationPage() {
{/* Day-of-week picker for specific_days */}
{frequency === 'specific_days' && (
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Days</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Days</label>
<div className="flex gap-2 flex-wrap">
{DAY_OPTIONS.map(({ value, label }) => (
<button
@@ -166,7 +166,7 @@ export default function NewMedicationPage() {
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
daysOfWeek.includes(value)
? 'bg-indigo-600 text-white border-indigo-600'
: 'bg-white text-gray-700 border-gray-300'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
{label}
@@ -180,22 +180,22 @@ export default function NewMedicationPage() {
{frequency === 'every_n_days' && (
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Every N Days</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Every N Days</label>
<input
type="number"
min={1}
value={intervalDays}
onChange={(e) => setIntervalDays(parseInt(e.target.value) || 1)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Starting From</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Starting From</label>
<input
type="date"
value={startDate}
onChange={(e) => setStartDate(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
</div>
@@ -205,17 +205,17 @@ export default function NewMedicationPage() {
{frequency !== 'as_needed' && (
<div>
<div className="flex items-center justify-between mb-1">
<label className="block text-sm font-medium text-gray-700">Times</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300">Times</label>
<button
type="button"
onClick={handleAddTime}
className="text-indigo-600 text-sm font-medium"
className="text-indigo-600 dark:text-indigo-400 text-sm font-medium"
>
+ Add Time
</button>
</div>
{frequency === 'daily' && (
<p className="text-xs text-gray-400 mb-2">Add multiple times for 2x, 3x, or more doses per day</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mb-2">Add multiple times for 2x, 3x, or more doses per day</p>
)}
<div className="space-y-2">
{times.map((time, index) => (
@@ -224,13 +224,13 @@ export default function NewMedicationPage() {
type="time"
value={time}
onChange={(e) => handleTimeChange(index, e.target.value)}
className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="flex-1 px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
{times.length > 1 && (
<button
type="button"
onClick={() => handleRemoveTime(index)}
className="text-red-500 px-3"
className="text-red-500 dark:text-red-400 px-3"
>
Remove
</button>

View File

@@ -214,7 +214,7 @@ export default function MedicationsPage() {
return (
<div className="p-4 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Medications</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Medications</h1>
<Link href="/dashboard/medications/new" className="bg-indigo-600 text-white p-2 rounded-full">
<PlusIcon size={24} />
</Link>
@@ -224,7 +224,7 @@ export default function MedicationsPage() {
<PushNotificationToggle />
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-lg">
<div className="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400 px-4 py-3 rounded-lg">
{error}
</div>
)}
@@ -232,38 +232,38 @@ export default function MedicationsPage() {
{/* Due Now Section */}
{dueEntries.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Due</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Due</h2>
<div className="space-y-3">
{dueEntries.map((entry) => (
<div
key={`${entry.item.medication.id}-${entry.time}`}
className={`bg-white rounded-xl p-4 shadow-sm ${borderColor(entry.status)}`}
className={`bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm ${borderColor(entry.status)}`}
>
<div className="flex items-center justify-between mb-2">
<div>
<div className="flex items-center gap-2">
<h3 className="font-semibold text-gray-900">{entry.item.medication.name}</h3>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{entry.item.medication.name}</h3>
{entry.item.is_previous_day && (
<span className="text-xs bg-purple-100 text-purple-700 px-2 py-0.5 rounded">Yesterday</span>
<span className="text-xs bg-purple-100 dark:bg-purple-900/30 text-purple-700 dark:text-purple-400 px-2 py-0.5 rounded">Yesterday</span>
)}
</div>
<p className="text-sm text-gray-500">{entry.item.medication.dosage} {entry.item.medication.unit}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{entry.item.medication.dosage} {entry.item.medication.unit}</p>
</div>
</div>
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-3">
<div className="flex items-center justify-between bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<div className="flex items-center gap-2">
<ClockIcon size={16} className="text-gray-500" />
<span className="font-medium">{entry.time}</span>
<ClockIcon size={16} className="text-gray-500 dark:text-gray-400" />
<span className="font-medium text-gray-900 dark:text-gray-100">{entry.time}</span>
{entry.status === 'overdue' && (
<span className="text-xs text-red-600 font-medium">Overdue</span>
<span className="text-xs text-red-600 dark:text-red-400 font-medium">Overdue</span>
)}
</div>
{entry.status === 'taken' ? (
<span className="text-green-600 font-medium flex items-center gap-1">
<span className="text-green-600 dark:text-green-400 font-medium flex items-center gap-1">
<CheckIcon size={16} /> Taken
</span>
) : entry.status === 'skipped' ? (
<span className="text-gray-400 font-medium">Skipped</span>
<span className="text-gray-400 dark:text-gray-500 font-medium">Skipped</span>
) : (
<div className="flex gap-2">
<button
@@ -274,7 +274,7 @@ export default function MedicationsPage() {
</button>
<button
onClick={() => handleSkip(entry.item.medication.id, entry.time)}
className="text-gray-500 px-2 py-1"
className="text-gray-500 dark:text-gray-400 px-2 py-1"
>
Skip
</button>
@@ -290,18 +290,18 @@ export default function MedicationsPage() {
{/* PRN Section */}
{prnEntries.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">As Needed</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">As Needed</h2>
<div className="space-y-3">
{prnEntries.map((item) => (
<div key={item.medication.id} className="bg-white rounded-xl p-4 shadow-sm">
<div key={item.medication.id} className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-2">
<div>
<h3 className="font-semibold text-gray-900">{item.medication.name}</h3>
<p className="text-sm text-gray-500">{item.medication.dosage} {item.medication.unit}</p>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{item.medication.name}</h3>
<p className="text-sm text-gray-500 dark:text-gray-400">{item.medication.dosage} {item.medication.unit}</p>
</div>
</div>
<div className="flex items-center justify-between bg-gray-50 rounded-lg p-3">
<span className="text-gray-500 text-sm">As needed</span>
<div className="flex items-center justify-between bg-gray-50 dark:bg-gray-700 rounded-lg p-3">
<span className="text-gray-500 dark:text-gray-400 text-sm">As needed</span>
<button
onClick={() => handleTake(item.medication.id)}
className="bg-green-600 text-white px-3 py-1 rounded-lg text-sm font-medium"
@@ -318,24 +318,24 @@ export default function MedicationsPage() {
{/* Upcoming Section */}
{upcomingEntries.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-500 mb-3">Upcoming</h2>
<h2 className="text-lg font-semibold text-gray-500 dark:text-gray-400 mb-3">Upcoming</h2>
<div className="space-y-3">
{upcomingEntries.map((entry) => (
<div
key={`${entry.item.medication.id}-${entry.time}`}
className="bg-gray-50 rounded-xl p-4 shadow-sm opacity-75"
className="bg-gray-50 dark:bg-gray-800 rounded-xl p-4 shadow-sm opacity-75"
>
<div className="flex items-center justify-between">
<div>
<div className="flex items-center gap-2">
<h3 className="font-medium text-gray-700">{entry.item.medication.name}</h3>
<h3 className="font-medium text-gray-700 dark:text-gray-300">{entry.item.medication.name}</h3>
{entry.item.is_next_day && (
<span className="text-xs bg-blue-100 text-blue-700 px-2 py-0.5 rounded">Tomorrow</span>
<span className="text-xs bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 px-2 py-0.5 rounded">Tomorrow</span>
)}
</div>
<p className="text-sm text-gray-400">{entry.item.medication.dosage} {entry.item.medication.unit}</p>
<p className="text-sm text-gray-400 dark:text-gray-500">{entry.item.medication.dosage} {entry.item.medication.unit}</p>
</div>
<div className="flex items-center gap-2 text-gray-400">
<div className="flex items-center gap-2 text-gray-400 dark:text-gray-500">
<ClockIcon size={16} />
<span className="font-medium">{entry.time}</span>
</div>
@@ -348,15 +348,15 @@ export default function MedicationsPage() {
{/* All Medications */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">All Medications</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">All Medications</h2>
{medications.length === 0 ? (
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<PillIcon className="text-gray-400" size={32} />
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
<PillIcon className="text-gray-400 dark:text-gray-500" size={32} />
</div>
<h3 className="font-semibold text-gray-900 mb-1">No medications yet</h3>
<p className="text-gray-500 text-sm mb-4">Add your medications to track them</p>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No medications yet</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm mb-4">Add your medications to track them</p>
<Link href="/dashboard/medications/new" className="inline-block bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium">
Add Medication
</Link>
@@ -366,41 +366,41 @@ export default function MedicationsPage() {
{medications.map((med) => {
const { percent: adherencePercent, isPrn } = getAdherenceForMed(med.id);
return (
<div key={med.id} className="bg-white rounded-xl p-4 shadow-sm">
<div key={med.id} className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-start justify-between">
<div className="flex-1">
<div className="flex items-center gap-2">
<h3 className="font-semibold text-gray-900">{med.name}</h3>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{med.name}</h3>
{!med.active && (
<span className="text-xs bg-gray-100 text-gray-500 px-2 py-0.5 rounded">Inactive</span>
<span className="text-xs bg-gray-100 dark:bg-gray-700 text-gray-500 dark:text-gray-400 px-2 py-0.5 rounded">Inactive</span>
)}
</div>
<p className="text-gray-500 text-sm">{med.dosage} {med.unit} &middot; {formatSchedule(med)}</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">{med.dosage} {med.unit} &middot; {formatSchedule(med)}</p>
{med.times.length > 0 && (
<p className="text-gray-400 text-sm mt-1">Times: {med.times.join(', ')}</p>
<p className="text-gray-400 dark:text-gray-500 text-sm mt-1">Times: {med.times.join(', ')}</p>
)}
</div>
<button
onClick={() => handleDelete(med.id)}
className="text-red-500 p-2"
className="text-red-500 dark:text-red-400 p-2"
>
<TrashIcon size={18} />
</button>
</div>
{/* Adherence */}
<div className="mt-3 pt-3 border-t border-gray-100">
<div className="mt-3 pt-3 border-t border-gray-100 dark:border-gray-700">
{isPrn || adherencePercent === null ? (
<span className="text-sm text-gray-400">PRN &mdash; no adherence tracking</span>
<span className="text-sm text-gray-400 dark:text-gray-500">PRN &mdash; no adherence tracking</span>
) : (
<>
<div className="flex items-center justify-between mb-1">
<span className="text-sm text-gray-500">30-day adherence</span>
<span className={`font-semibold ${adherencePercent >= 80 ? 'text-green-600' : adherencePercent >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
<span className="text-sm text-gray-500 dark:text-gray-400">30-day adherence</span>
<span className={`font-semibold ${adherencePercent >= 80 ? 'text-green-600 dark:text-green-400' : adherencePercent >= 50 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400'}`}>
{adherencePercent}%
</span>
</div>
<div className="h-2 bg-gray-100 rounded-full overflow-hidden">
<div className="h-2 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden">
<div
className={`h-full ${adherencePercent >= 80 ? 'bg-green-500' : adherencePercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
style={{ width: `${adherencePercent}%` }}

View File

@@ -141,7 +141,7 @@ export default function DashboardPage() {
<div className="p-4 space-y-6">
{/* Active Session Banner */}
{activeSession && activeSession.session.status === 'active' && (
<div className="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-2xl p-4 text-white ring-2 ring-indigo-400/50 ring-offset-2 ring-offset-gray-50 animate-gentle-pulse">
<div className="bg-gradient-to-r from-indigo-500 to-purple-600 rounded-2xl p-4 text-white ring-2 ring-indigo-400/50 ring-offset-2 ring-offset-gray-50 dark:ring-offset-gray-950 animate-gentle-pulse">
<div className="flex items-center justify-between">
<div>
<p className="text-white/80 text-sm">Continue where you left off</p>
@@ -162,8 +162,8 @@ export default function DashboardPage() {
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900">{getGreeting()}, {user?.username}!</h1>
<p className="text-gray-500 mt-1">Let&apos;s build some great habits today.</p>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{getGreeting()}, {user?.username}!</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">Let&apos;s build some great habits today.</p>
</div>
{/* "Never miss twice" Recovery Cards */}
@@ -171,11 +171,11 @@ export default function DashboardPage() {
const routine = routines.find(r => r.id === recovery.routine_id);
if (!routine) return null;
return (
<div key={recovery.routine_id} className="bg-amber-50 border border-amber-200 rounded-2xl p-4">
<div key={recovery.routine_id} className="bg-amber-50 dark:bg-amber-900/30 border border-amber-200 dark:border-amber-800 rounded-2xl p-4">
<div className="flex items-center justify-between">
<div>
<p className="text-amber-800 text-sm font-medium mb-1">Welcome back</p>
<p className="text-amber-700 text-sm">
<p className="text-amber-800 dark:text-amber-200 text-sm font-medium mb-1">Welcome back</p>
<p className="text-amber-700 dark:text-amber-300 text-sm">
It&apos;s been a couple days since {routine.icon} {routine.name}. That&apos;s completely okay picking it back up today is what matters most.
</p>
</div>
@@ -193,44 +193,44 @@ export default function DashboardPage() {
{/* Weekly Stats — identity-based language */}
{weeklySummary && weeklySummary.total_completed > 0 && (
<div className="grid grid-cols-3 gap-3">
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-center gap-2 text-indigo-600 mb-1">
<StarIcon size={18} />
<span className="text-xs font-medium">Done</span>
</div>
<p className="text-2xl font-bold text-gray-900">{weeklySummary.total_completed}</p>
<p className="text-xs text-gray-400 mt-0.5">this week</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">{weeklySummary.total_completed}</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">this week</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-center gap-2 text-purple-600 mb-1">
<ClockIcon size={18} />
<span className="text-xs font-medium">Invested</span>
</div>
<p className="text-2xl font-bold text-gray-900">{formatTime(weeklySummary.total_time_minutes)}</p>
<p className="text-xs text-gray-400 mt-0.5">in yourself</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">{formatTime(weeklySummary.total_time_minutes)}</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">in yourself</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-center gap-2 text-pink-600 mb-1">
<ActivityIcon size={18} />
<span className="text-xs font-medium">Active</span>
</div>
<p className="text-2xl font-bold text-gray-900">{weeklySummary.routines_started}</p>
<p className="text-xs text-gray-400 mt-0.5">routines</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">{weeklySummary.routines_started}</p>
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">routines</p>
</div>
</div>
)}
{/* Quick Start Routines */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Your Routines</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Your Routines</h2>
{routines.length === 0 ? (
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<FlameIcon className="text-gray-400" size={32} />
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
<FlameIcon className="text-gray-400 dark:text-gray-500" size={32} />
</div>
<h3 className="font-semibold text-gray-900 mb-1">No routines yet</h3>
<p className="text-gray-500 text-sm mb-4">Create your first routine to get started</p>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No routines yet</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm mb-4">Create your first routine to get started</p>
<Link
href="/dashboard/routines/new"
className="inline-block bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium"
@@ -245,21 +245,21 @@ export default function DashboardPage() {
return (
<div
key={routine.id}
className="bg-white rounded-xl p-4 shadow-sm flex items-center justify-between"
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm flex items-center justify-between"
>
<div className="flex items-center gap-3">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-100 to-pink-100 rounded-xl flex items-center justify-center">
<div className="w-12 h-12 bg-gradient-to-br from-indigo-100 to-pink-100 dark:from-indigo-900/50 dark:to-pink-900/50 rounded-xl flex items-center justify-center">
<span className="text-2xl">{routine.icon || '✨'}</span>
</div>
<div>
<h3 className="font-semibold text-gray-900">{routine.name}</h3>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{routine.name}</h3>
{streak && streak.current_streak > 0 ? (
<p className="text-sm text-orange-500 flex items-center gap-1">
<FlameIcon size={14} />
{streak.current_streak} day{streak.current_streak !== 1 ? 's' : ''}
</p>
) : routine.description ? (
<p className="text-gray-500 text-sm">{routine.description}</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">{routine.description}</p>
) : null}
</div>
</div>
@@ -293,7 +293,7 @@ export default function DashboardPage() {
</div>
{error && (
<div className="bg-amber-50 text-amber-700 px-4 py-3 rounded-lg text-sm">
<div className="bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}

View File

@@ -105,24 +105,24 @@ export default function LaunchScreen() {
if (!routine) return null;
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="flex items-center gap-3 px-4 py-3">
<button onClick={() => router.back()} className="p-1">
<button onClick={() => router.back()} className="p-1 text-gray-600 dark:text-gray-400">
<ArrowLeftIcon size={24} />
</button>
<h1 className="text-xl font-bold text-gray-900">Ready to start</h1>
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">Ready to start</h1>
</div>
</header>
<div className="p-4 space-y-6">
{/* Routine info */}
<div className="text-center py-4">
<div className="w-20 h-20 bg-gradient-to-br from-indigo-100 to-pink-100 rounded-2xl flex items-center justify-center mx-auto mb-3">
<div className="w-20 h-20 bg-gradient-to-br from-indigo-100 to-pink-100 dark:from-indigo-900/50 dark:to-pink-900/50 rounded-2xl flex items-center justify-center mx-auto mb-3">
<span className="text-5xl">{routine.icon || '✨'}</span>
</div>
<h2 className="text-2xl font-bold text-gray-900">{routine.name}</h2>
<div className="flex items-center justify-center gap-2 text-gray-500 mt-2">
<h2 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{routine.name}</h2>
<div className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400 mt-2">
<ClockIcon size={16} />
<span>~{totalDuration} minutes</span>
<span>·</span>
@@ -132,15 +132,15 @@ export default function LaunchScreen() {
{/* Implementation Intention */}
{(routine.habit_stack_after || schedule) && (
<div className="bg-indigo-50 border border-indigo-200 rounded-2xl p-4">
<div className="bg-indigo-50 dark:bg-indigo-900/30 border border-indigo-200 dark:border-indigo-800 rounded-2xl p-4">
{routine.habit_stack_after && (
<p className="text-indigo-800 text-sm mb-2">
<p className="text-indigo-800 dark:text-indigo-200 text-sm mb-2">
After <span className="font-semibold">{routine.habit_stack_after}</span>, I will start{' '}
<span className="font-semibold">{routine.name}</span>
</p>
)}
{schedule && (
<p className="text-indigo-700 text-sm">
<p className="text-indigo-700 dark:text-indigo-300 text-sm">
{schedule.days.length > 0 && (
<span className="font-semibold">
{schedule.days.length === 7 ? 'Every day' :
@@ -161,8 +161,8 @@ export default function LaunchScreen() {
{/* Environment Check */}
{envPrompts.length > 0 && (
<div className="bg-white rounded-2xl p-4 shadow-sm">
<h3 className="font-semibold text-gray-900 mb-3">Quick check</h3>
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">Quick check</h3>
<div className="space-y-2">
{envPrompts.map((prompt, i) => (
<button
@@ -170,18 +170,18 @@ export default function LaunchScreen() {
onClick={() => toggleEnvironmentCheck(i)}
className={`w-full flex items-center gap-3 p-3 rounded-xl transition ${
environmentChecked.has(i)
? 'bg-green-50 border border-green-200'
: 'bg-gray-50 border border-gray-200'
? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800'
: 'bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600'
}`}
>
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
environmentChecked.has(i)
? 'border-green-500 bg-green-500'
: 'border-gray-300'
: 'border-gray-300 dark:border-gray-500'
}`}>
{environmentChecked.has(i) && <CheckIcon size={14} className="text-white" />}
</div>
<span className={`text-sm ${environmentChecked.has(i) ? 'text-green-800' : 'text-gray-700'}`}>
<span className={`text-sm ${environmentChecked.has(i) ? 'text-green-800 dark:text-green-200' : 'text-gray-700 dark:text-gray-300'}`}>
{prompt}
</span>
</button>
@@ -191,11 +191,11 @@ export default function LaunchScreen() {
)}
{/* Emotion Bridge */}
<div className="bg-white rounded-2xl p-4 shadow-sm">
<h3 className="font-semibold text-gray-900 mb-1">
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
When you finish in ~{totalDuration} minutes, how will you feel?
</h3>
<p className="text-gray-500 text-sm mb-3">You <em>get to</em> do this</p>
<p className="text-gray-500 dark:text-gray-400 text-sm mb-3">You <em>get to</em> do this</p>
<div className="grid grid-cols-2 gap-2">
{EMOTION_OPTIONS.map((option) => (
<button
@@ -203,12 +203,12 @@ export default function LaunchScreen() {
onClick={() => setSelectedEmotion(option.label)}
className={`flex items-center gap-2 p-3 rounded-xl border transition ${
selectedEmotion === option.label
? 'border-indigo-500 bg-indigo-50'
: 'border-gray-200 hover:border-gray-300'
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/30'
: 'border-gray-200 dark:border-gray-600 hover:border-gray-300 dark:hover:border-gray-500'
}`}
>
<span className="text-xl">{option.emoji}</span>
<span className="text-sm font-medium text-gray-900">{option.label}</span>
<span className="text-sm font-medium text-gray-900 dark:text-gray-100">{option.label}</span>
</button>
))}
</div>

View File

@@ -241,14 +241,14 @@ export default function RoutineDetailPage() {
if (!routine) return null;
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="flex items-center justify-between px-4 py-3">
<div className="flex items-center gap-3">
<button onClick={() => router.back()} className="p-1">
<button onClick={() => router.back()} className="p-1 text-gray-600 dark:text-gray-400">
<ArrowLeftIcon size={24} />
</button>
<h1 className="text-xl font-bold text-gray-900">
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">
{isEditing ? 'Edit Routine' : routine.name}
</h1>
</div>
@@ -268,13 +268,13 @@ export default function RoutineDetailPage() {
}
}}
disabled={isDeleting}
className="text-red-500 font-medium disabled:opacity-50"
className="text-red-500 dark:text-red-400 font-medium disabled:opacity-50"
>
{isDeleting ? 'Deleting...' : 'Delete'}
</button>
<button
onClick={() => setIsEditing(true)}
className="text-indigo-600 font-medium"
className="text-indigo-600 dark:text-indigo-400 font-medium"
>
Edit
</button>
@@ -286,9 +286,9 @@ export default function RoutineDetailPage() {
<div className="p-4 space-y-6">
{isEditing ? (
<>
<div className="bg-white rounded-xl p-4 shadow-sm space-y-4">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Icon</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Icon</label>
<div className="flex flex-wrap gap-2">
{ICONS.map((i) => (
<button
@@ -297,8 +297,8 @@ export default function RoutineDetailPage() {
onClick={() => setEditIcon(i)}
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition ${
editIcon === i
? 'bg-indigo-100 ring-2 ring-indigo-600'
: 'bg-gray-100 hover:bg-gray-200'
? 'bg-indigo-100 dark:bg-indigo-900/50 ring-2 ring-indigo-600'
: 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{i}
@@ -307,53 +307,53 @@ export default function RoutineDetailPage() {
</div>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
<input
type="text"
value={editName}
onChange={(e) => setEditName(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description</label>
<input
type="text"
value={editDescription}
onChange={(e) => setEditDescription(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Location</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Location</label>
<input
type="text"
value={editLocation}
onChange={(e) => setEditLocation(e.target.value)}
placeholder="Where do you do this? e.g., bathroom, kitchen"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Anchor habit</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Anchor habit</label>
<input
type="text"
value={editHabitStack}
onChange={(e) => setEditHabitStack(e.target.value)}
placeholder="What do you do right before? e.g., finish breakfast"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Environment prompts</label>
<p className="text-xs text-gray-500 mb-2">Quick checklist shown before starting</p>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Environment prompts</label>
<p className="text-xs text-gray-500 dark:text-gray-400 mb-2">Quick checklist shown before starting</p>
<div className="space-y-2 mb-2">
{editEnvPrompts.map((prompt, i) => (
<div key={i} className="flex items-center gap-2">
<span className="flex-1 text-sm text-gray-700 bg-gray-50 px-3 py-2 rounded-lg">{prompt}</span>
<span className="flex-1 text-sm text-gray-700 dark:text-gray-300 bg-gray-50 dark:bg-gray-700 px-3 py-2 rounded-lg">{prompt}</span>
<button
onClick={() => setEditEnvPrompts(editEnvPrompts.filter((_, j) => j !== i))}
className="text-red-500 text-sm px-2"
className="text-red-500 dark:text-red-400 text-sm px-2"
>
Remove
</button>
@@ -366,7 +366,7 @@ export default function RoutineDetailPage() {
value={newEnvPrompt}
onChange={(e) => setNewEnvPrompt(e.target.value)}
placeholder="e.g., Water bottle nearby?"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-indigo-500 outline-none"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
onKeyDown={(e) => {
if (e.key === 'Enter' && newEnvPrompt.trim()) {
setEditEnvPrompts([...editEnvPrompts, newEnvPrompt.trim()]);
@@ -381,7 +381,7 @@ export default function RoutineDetailPage() {
setNewEnvPrompt('');
}
}}
className="px-3 py-2 bg-gray-200 rounded-lg text-sm font-medium"
className="px-3 py-2 bg-gray-200 dark:bg-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300"
>
Add
</button>
@@ -401,7 +401,7 @@ export default function RoutineDetailPage() {
setEditHabitStack(routine.habit_stack_after || '');
setEditEnvPrompts(routine.environment_prompts || []);
}}
className="flex-1 px-4 py-2 border border-gray-300 rounded-lg font-medium"
className="flex-1 px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg font-medium text-gray-700 dark:text-gray-300"
>
Cancel
</button>
@@ -415,17 +415,17 @@ export default function RoutineDetailPage() {
</>
) : (
<>
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-center gap-4">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-100 to-pink-100 rounded-2xl flex items-center justify-center">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-100 to-pink-100 dark:from-indigo-900/50 dark:to-pink-900/50 rounded-2xl flex items-center justify-center">
<span className="text-4xl">{routine.icon || '✨'}</span>
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-gray-900">{routine.name}</h2>
<h2 className="text-xl font-bold text-gray-900 dark:text-gray-100">{routine.name}</h2>
{routine.description && (
<p className="text-gray-500">{routine.description}</p>
<p className="text-gray-500 dark:text-gray-400">{routine.description}</p>
)}
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500">
<div className="flex items-center gap-4 mt-2 text-sm text-gray-500 dark:text-gray-400">
<span className="flex items-center gap-1">
<ClockIcon size={14} />
{totalDuration} min
@@ -444,16 +444,16 @@ export default function RoutineDetailPage() {
</div>
{/* Schedule display (view mode) */}
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<ClockIcon size={16} className="text-indigo-500" />
<h3 className="font-semibold text-gray-900">Schedule</h3>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">Schedule</h3>
</div>
{!showScheduleEditor && (
<button
onClick={() => setShowScheduleEditor(true)}
className="text-indigo-600 text-sm font-medium"
className="text-indigo-600 dark:text-indigo-400 text-sm font-medium"
>
{schedule ? 'Edit' : 'Add schedule'}
</button>
@@ -468,7 +468,7 @@ export default function RoutineDetailPage() {
type="button"
onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
editDays.length === 7 ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-300'
editDays.length === 7 ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
Every day
@@ -477,7 +477,7 @@ export default function RoutineDetailPage() {
type="button"
onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
editDays.length === 5 && !editDays.includes('sat') && !editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-300'
editDays.length === 5 && !editDays.includes('sat') && !editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
Weekdays
@@ -486,7 +486,7 @@ export default function RoutineDetailPage() {
type="button"
onClick={() => setEditDays(['sat', 'sun'])}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
editDays.length === 2 && editDays.includes('sat') && editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-300'
editDays.length === 2 && editDays.includes('sat') && editDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
Weekends
@@ -494,7 +494,7 @@ export default function RoutineDetailPage() {
</div>
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700 mb-2">Days</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Days</label>
<div className="flex gap-2 flex-wrap">
{DAY_OPTIONS.map((day) => (
<button
@@ -502,9 +502,9 @@ export default function RoutineDetailPage() {
type="button"
onClick={() => toggleDay(day.value)}
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
editDays.includes(day.value)
? 'bg-indigo-600 text-white border-indigo-600'
: 'bg-white text-gray-700 border-gray-300'
editDays.includes(day.value)
? 'bg-indigo-600 text-white border-indigo-600'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
{day.label}
@@ -513,23 +513,23 @@ export default function RoutineDetailPage() {
</div>
</div>
<div className="mb-3">
<label className="block text-sm font-medium text-gray-700 mb-1">Time</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Time</label>
<input
type="time"
value={editTime}
onChange={(e) => setEditTime(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div className="flex items-center justify-between mb-3">
<div>
<p className="font-medium text-gray-900">Send reminder</p>
<p className="text-sm text-gray-500">Get notified when it's time</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Send reminder</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Get notified when it's time</p>
</div>
<button
onClick={() => setEditRemind(!editRemind)}
className={`w-12 h-7 rounded-full transition-colors ${
editRemind ? 'bg-indigo-500' : 'bg-gray-300'
editRemind ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -551,7 +551,7 @@ export default function RoutineDetailPage() {
setEditRemind(true);
}
}}
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm font-medium"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg text-sm font-medium text-gray-700 dark:text-gray-300"
>
Cancel
</button>
@@ -565,15 +565,15 @@ export default function RoutineDetailPage() {
</>
) : schedule && schedule.days.length > 0 ? (
<>
<p className="text-gray-700">
<p className="text-gray-700 dark:text-gray-300">
{formatDays(schedule.days)} at {schedule.time}
</p>
{schedule.remind && (
<p className="text-sm text-gray-500 mt-1">Reminders on</p>
<p className="text-sm text-gray-500 dark:text-gray-400 mt-1">Reminders on</p>
)}
</>
) : (
<p className="text-gray-500 text-sm">Not scheduled. Click "Add schedule" to set a time.</p>
<p className="text-gray-500 dark:text-gray-400 text-sm">Not scheduled. Click "Add schedule" to set a time.</p>
)}
</div>
</>
@@ -582,30 +582,30 @@ export default function RoutineDetailPage() {
{/* Steps */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-gray-900">Steps</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Steps</h2>
{steps.length >= 4 && steps.length <= 7 && (
<span className="text-xs text-green-600 bg-green-50 px-2 py-1 rounded-full">Good length</span>
<span className="text-xs text-green-600 dark:text-green-400 bg-green-50 dark:bg-green-900/30 px-2 py-1 rounded-full">Good length</span>
)}
</div>
{steps.length > 7 && (
<p className="text-sm text-amber-600 bg-amber-50 px-3 py-2 rounded-lg mb-3">
<p className="text-sm text-amber-600 dark:text-amber-400 bg-amber-50 dark:bg-amber-900/30 px-3 py-2 rounded-lg mb-3">
Tip: Routines with 4-7 steps tend to feel more manageable. Consider combining related steps.
</p>
)}
<div className="bg-white rounded-xl p-4 shadow-sm space-y-3 mb-4">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-3 mb-4">
<div className="flex gap-2">
<input
type="text"
value={newStepName}
onChange={(e) => setNewStepName(e.target.value)}
placeholder="New step name"
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="flex-1 px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
<select
value={newStepDuration}
onChange={(e) => setNewStepDuration(Number(e.target.value))}
className="px-3 py-2 border border-gray-300 rounded-lg"
className="px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value={1}>1m</option>
<option value={5}>5m</option>
@@ -623,44 +623,44 @@ export default function RoutineDetailPage() {
</div>
{steps.length === 0 ? (
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
<p className="text-gray-500">No steps yet</p>
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
<p className="text-gray-500 dark:text-gray-400">No steps yet</p>
</div>
) : (
<div className="space-y-2">
{steps.map((step, index) => (
<div
key={step.id}
className="bg-white rounded-xl p-4 shadow-sm flex items-center gap-3"
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm flex items-center gap-3"
>
<div className="w-8 h-8 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center font-semibold text-sm">
<div className="w-8 h-8 bg-indigo-100 dark:bg-indigo-900/50 text-indigo-600 dark:text-indigo-400 rounded-full flex items-center justify-center font-semibold text-sm">
{index + 1}
</div>
<div className="flex-1">
<h3 className="font-medium text-gray-900">{step.name}</h3>
<h3 className="font-medium text-gray-900 dark:text-gray-100">{step.name}</h3>
{step.duration_minutes && (
<p className="text-sm text-gray-500">{step.duration_minutes} min</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{step.duration_minutes} min</p>
)}
</div>
<div className="flex flex-col">
<button
onClick={() => handleMoveStep(step.id, 'up')}
disabled={index === 0}
className="text-gray-400 p-1 disabled:opacity-30 hover:text-gray-600"
className="text-gray-400 dark:text-gray-500 p-1 disabled:opacity-30 hover:text-gray-600 dark:hover:text-gray-300"
>
</button>
<button
onClick={() => handleMoveStep(step.id, 'down')}
disabled={index === steps.length - 1}
className="text-gray-400 p-1 disabled:opacity-30 hover:text-gray-600"
className="text-gray-400 dark:text-gray-500 p-1 disabled:opacity-30 hover:text-gray-600 dark:hover:text-gray-300"
>
</button>
</div>
<button
onClick={() => handleDeleteStep(step.id)}
className="text-red-500 p-2"
className="text-red-500 dark:text-red-400 p-2"
>
<TrashIcon size={18} />
</button>

View File

@@ -116,26 +116,26 @@ export default function NewRoutinePage() {
};
return (
<div className="min-h-screen bg-gray-50">
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
<header className="bg-white dark:bg-gray-900 border-b border-gray-200 dark:border-gray-700 sticky top-0 z-10">
<div className="flex items-center gap-3 px-4 py-3">
<button onClick={() => router.back()} className="p-1">
<button onClick={() => router.back()} className="p-1 text-gray-600 dark:text-gray-400">
<ArrowLeftIcon size={24} />
</button>
<h1 className="text-xl font-bold text-gray-900">New Routine</h1>
<h1 className="text-xl font-bold text-gray-900 dark:text-gray-100">New Routine</h1>
</div>
</header>
<Link
href="/dashboard/templates"
className="mx-4 mt-4 flex items-center gap-3 bg-gradient-to-r from-indigo-50 to-purple-50 border-2 border-indigo-200 rounded-xl p-4 hover:border-indigo-400 transition-colors"
className="mx-4 mt-4 flex items-center gap-3 bg-gradient-to-r from-indigo-50 to-purple-50 dark:from-indigo-900/30 dark:to-purple-900/30 border-2 border-indigo-200 dark:border-indigo-800 rounded-xl p-4 hover:border-indigo-400 dark:hover:border-indigo-600 transition-colors"
>
<div className="w-12 h-12 bg-indigo-100 rounded-xl flex items-center justify-center flex-shrink-0">
<CopyIcon size={24} className="text-indigo-600" />
<div className="w-12 h-12 bg-indigo-100 dark:bg-indigo-900/50 rounded-xl flex items-center justify-center flex-shrink-0">
<CopyIcon size={24} className="text-indigo-600 dark:text-indigo-400" />
</div>
<div className="flex-1">
<p className="font-semibold text-gray-900">Start from a template</p>
<p className="text-sm text-gray-500">Browse pre-made routines</p>
<p className="font-semibold text-gray-900 dark:text-gray-100">Start from a template</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Browse pre-made routines</p>
</div>
<div className="bg-indigo-600 text-white text-xs font-medium px-2 py-1 rounded-full">
Recommended
@@ -144,15 +144,15 @@ export default function NewRoutinePage() {
<form onSubmit={handleSubmit} className="p-4 space-y-6">
{error && (
<div className="bg-red-50 text-red-600 px-4 py-3 rounded-lg text-sm">
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
{/* Basic Info */}
<div className="bg-white rounded-xl p-4 shadow-sm space-y-4">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Icon</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Icon</label>
<div className="flex flex-wrap gap-2">
{ICONS.map((i) => (
<button
@@ -161,8 +161,8 @@ export default function NewRoutinePage() {
onClick={() => setIcon(i)}
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition ${
icon === i
? 'bg-indigo-100 ring-2 ring-indigo-600'
: 'bg-gray-100 hover:bg-gray-200'
? 'bg-indigo-100 dark:bg-indigo-900/50 ring-2 ring-indigo-600'
: 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'
}`}
>
{i}
@@ -172,31 +172,31 @@ export default function NewRoutinePage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Name</label>
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="Morning Routine"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Description (optional)</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Description (optional)</label>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Start your day right"
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
</div>
{/* Schedule */}
<div className="bg-white rounded-xl p-4 shadow-sm space-y-4">
<h2 className="text-lg font-semibold text-gray-900">Schedule <span className="text-sm font-normal text-gray-400">(optional)</span></h2>
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm space-y-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Schedule <span className="text-sm font-normal text-gray-400">(optional)</span></h2>
{/* Quick select buttons */}
<div className="flex gap-2">
@@ -204,7 +204,7 @@ export default function NewRoutinePage() {
type="button"
onClick={() => setScheduleDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
scheduleDays.length === 7 ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-300'
scheduleDays.length === 7 ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
Every day
@@ -213,7 +213,7 @@ export default function NewRoutinePage() {
type="button"
onClick={() => setScheduleDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
scheduleDays.length === 5 && !scheduleDays.includes('sat') && !scheduleDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-300'
scheduleDays.length === 5 && !scheduleDays.includes('sat') && !scheduleDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
Weekdays
@@ -222,7 +222,7 @@ export default function NewRoutinePage() {
type="button"
onClick={() => setScheduleDays(['sat', 'sun'])}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
scheduleDays.length === 2 && scheduleDays.includes('sat') && scheduleDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white text-gray-700 border-gray-300'
scheduleDays.length === 2 && scheduleDays.includes('sat') && scheduleDays.includes('sun') ? 'bg-indigo-600 text-white border-indigo-600' : 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
Weekends
@@ -230,7 +230,7 @@ export default function NewRoutinePage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-2">Days</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Days</label>
<div className="flex gap-2 flex-wrap">
{DAY_OPTIONS.map((day) => (
<button
@@ -240,7 +240,7 @@ export default function NewRoutinePage() {
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
scheduleDays.includes(day.value)
? 'bg-indigo-600 text-white border-indigo-600'
: 'bg-white text-gray-700 border-gray-300'
: 'bg-white dark:bg-gray-700 text-gray-700 dark:text-gray-300 border-gray-300 dark:border-gray-600'
}`}
>
{day.label}
@@ -250,25 +250,25 @@ export default function NewRoutinePage() {
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Time</label>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">Time</label>
<input
type="time"
value={scheduleTime}
onChange={(e) => setScheduleTime(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
</div>
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Send reminder</p>
<p className="text-sm text-gray-500">Get notified when it's time</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Send reminder</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Get notified when it's time</p>
</div>
<button
type="button"
onClick={() => setScheduleRemind(!scheduleRemind)}
className={`w-12 h-7 rounded-full transition-colors ${
scheduleRemind ? 'bg-indigo-500' : 'bg-gray-300'
scheduleRemind ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -281,11 +281,11 @@ export default function NewRoutinePage() {
{/* Steps */}
<div>
<div className="flex items-center justify-between mb-3">
<h2 className="text-lg font-semibold text-gray-900">Steps</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100">Steps</h2>
<button
type="button"
onClick={handleAddStep}
className="text-indigo-600 text-sm font-medium flex items-center gap-1"
className="text-indigo-600 dark:text-indigo-400 text-sm font-medium flex items-center gap-1"
>
<PlusIcon size={16} />
Add Step
@@ -293,12 +293,12 @@ export default function NewRoutinePage() {
</div>
{steps.length === 0 ? (
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
<p className="text-gray-500 mb-4">Add steps to your routine</p>
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
<p className="text-gray-500 dark:text-gray-400 mb-4">Add steps to your routine</p>
<button
type="button"
onClick={handleAddStep}
className="text-indigo-600 font-medium"
className="text-indigo-600 dark:text-indigo-400 font-medium"
>
+ Add your first step
</button>
@@ -308,9 +308,9 @@ export default function NewRoutinePage() {
{steps.map((step, index) => (
<div
key={step.id}
className="bg-white rounded-xl p-4 shadow-sm flex items-start gap-3"
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm flex items-start gap-3"
>
<div className="pt-3 text-gray-400 cursor-grab">
<div className="pt-3 text-gray-400 dark:text-gray-500 cursor-grab">
<GripVerticalIcon size={20} />
</div>
<div className="flex-1 space-y-3">
@@ -319,14 +319,14 @@ export default function NewRoutinePage() {
value={step.name}
onChange={(e) => handleUpdateStep(index, { name: e.target.value })}
placeholder="Step name"
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 outline-none"
className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 outline-none"
/>
<div className="flex items-center gap-3">
<label className="text-sm text-gray-500">Duration:</label>
<label className="text-sm text-gray-500 dark:text-gray-400">Duration:</label>
<select
value={step.duration_minutes || 5}
onChange={(e) => handleUpdateStep(index, { duration_minutes: Number(e.target.value) })}
className="px-3 py-1 border border-gray-300 rounded-lg text-sm"
className="px-3 py-1 border border-gray-300 dark:border-gray-600 rounded-lg text-sm bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value={1}>1 min</option>
<option value={2}>2 min</option>
@@ -343,7 +343,7 @@ export default function NewRoutinePage() {
<button
type="button"
onClick={() => handleDeleteStep(index)}
className="text-red-500 p-2"
className="text-red-500 dark:text-red-400 p-2"
>
<TrashIcon size={18} />
</button>

View File

@@ -475,10 +475,10 @@ export default function RoutinesPage() {
return (
// h-screen + overflow-hidden eliminates the outer page scrollbar;
// only the inner timeline div scrolls.
<div className="flex flex-col h-screen overflow-hidden bg-gray-50">
<div className="flex flex-col h-screen overflow-hidden bg-gray-50 dark:bg-gray-950">
{/* Header */}
<div className="flex items-center justify-between px-4 pt-4 pb-2 bg-white border-b border-gray-100 flex-shrink-0">
<h1 className="text-2xl font-bold text-gray-900">Routines</h1>
<div className="flex items-center justify-between px-4 pt-4 pb-2 bg-white dark:bg-gray-900 border-b border-gray-100 dark:border-gray-800 flex-shrink-0">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Routines</h1>
<Link
href="/dashboard/routines/new"
className="bg-indigo-600 text-white p-2 rounded-full"
@@ -516,7 +516,7 @@ export default function RoutinesPage() {
)}
{/* Week Strip */}
<div className="flex bg-white px-2 pb-3 pt-2 gap-1 border-b border-gray-100 flex-shrink-0">
<div className="flex bg-white dark:bg-gray-900 px-2 pb-3 pt-2 gap-1 border-b border-gray-100 dark:border-gray-800 flex-shrink-0">
{weekDays.map((day, i) => {
const selected = isSameDay(day, selectedDate);
const isTodayDay = isSameDay(day, today);
@@ -526,7 +526,7 @@ export default function RoutinesPage() {
onClick={() => setSelectedDate(day)}
className="flex-1 flex flex-col items-center py-1 rounded-xl"
>
<span className="text-xs text-gray-500 mb-1">
<span className="text-xs text-gray-500 dark:text-gray-400 mb-1">
{DAY_LABELS[i]}
</span>
<span
@@ -536,8 +536,8 @@ export default function RoutinesPage() {
: isTodayDay
? 'text-indigo-600 font-bold'
: selected
? 'bg-gray-200 text-gray-900'
: 'text-gray-700'
? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100'
: 'text-gray-700 dark:text-gray-300'
}`}
>
{day.getDate()}
@@ -556,10 +556,10 @@ export default function RoutinesPage() {
{allRoutines.length === 0 ? (
<div className="flex flex-col items-center justify-center py-20 px-4">
<div className="w-16 h-16 bg-indigo-100 rounded-full flex items-center justify-center mb-4">
<div className="w-16 h-16 bg-indigo-100 dark:bg-indigo-900/50 rounded-full flex items-center justify-center mb-4">
<ClockIcon size={32} className="text-indigo-400" />
</div>
<p className="text-gray-500 text-center mb-4">No routines yet</p>
<p className="text-gray-500 dark:text-gray-400 text-center mb-4">No routines yet</p>
<Link
href="/dashboard/routines/new"
className="bg-indigo-600 text-white px-6 py-3 rounded-xl font-medium"
@@ -593,10 +593,10 @@ export default function RoutinesPage() {
height: `${HOUR_HEIGHT}px`,
}}
>
<span className="w-14 text-xs text-gray-400 pr-2 text-right flex-shrink-0 -mt-2">
<span className="w-14 text-xs text-gray-400 dark:text-gray-500 pr-2 text-right flex-shrink-0 -mt-2">
{label}
</span>
<div className="flex-1 border-t border-gray-200 h-full" />
<div className="flex-1 border-t border-gray-200 dark:border-gray-700 h-full" />
</div>
);
}
@@ -642,8 +642,8 @@ export default function RoutinesPage() {
}}
className={`absolute rounded-xl px-3 py-2 text-left shadow-sm border transition-all overflow-hidden ${
isPast
? 'bg-green-50 border-green-200 opacity-75'
: 'bg-indigo-50 border-indigo-200'
? 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800 opacity-75'
: 'bg-indigo-50 dark:bg-indigo-900/30 border-indigo-200 dark:border-indigo-800'
} ${isCurrent ? 'ring-2 ring-indigo-500 opacity-100' : ''}`}
>
<div className="flex items-center gap-2">
@@ -651,10 +651,10 @@ export default function RoutinesPage() {
{entry.routine_icon || '✨'}
</span>
<div className="min-w-0 flex-1">
<p className="font-semibold text-gray-900 text-sm truncate">
<p className="font-semibold text-gray-900 dark:text-gray-100 text-sm truncate">
{entry.routine_name}
</p>
<p className="text-xs text-gray-500 truncate">
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{formatTime(entry.time)}
{entry.total_duration_minutes > 0 && (
<>
@@ -687,13 +687,13 @@ export default function RoutinesPage() {
totalLanes: 1,
};
let statusColor = 'bg-blue-50 border-blue-200';
let statusColor = 'bg-blue-50 dark:bg-blue-900/30 border-blue-200 dark:border-blue-800';
if (group.allTaken)
statusColor = 'bg-green-50 border-green-200';
statusColor = 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800';
else if (group.allSkipped)
statusColor = 'bg-gray-50 border-gray-200 opacity-60';
statusColor = 'bg-gray-50 dark:bg-gray-800/50 border-gray-200 dark:border-gray-700 opacity-60';
else if (group.anyOverdue)
statusColor = 'bg-amber-50 border-amber-300';
statusColor = 'bg-amber-50 dark:bg-amber-900/30 border-amber-300 dark:border-amber-700';
return (
<div
@@ -711,10 +711,10 @@ export default function RoutinesPage() {
💊
</span>
<div className="min-w-0 flex-1">
<p className="font-semibold text-gray-900 text-sm truncate">
<p className="font-semibold text-gray-900 dark:text-gray-100 text-sm truncate">
{formatMedsList(group.medications)}
</p>
<p className="text-xs text-gray-500 truncate">
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{formatTime(group.time)}
</p>
</div>
@@ -768,7 +768,7 @@ export default function RoutinesPage() {
}
});
}}
className="text-gray-500 px-1 py-1 text-xs"
className="text-gray-500 dark:text-gray-400 px-1 py-1 text-xs"
>
Skip
</button>
@@ -793,23 +793,23 @@ export default function RoutinesPage() {
{/* Unscheduled routines — inside the scroll area, below the timeline */}
{unscheduledRoutines.length > 0 && (
<div className="border-t border-gray-200 bg-white px-4 pt-3 pb-4">
<h2 className="text-xs font-semibold text-gray-500 uppercase tracking-wide mb-2">
<div className="border-t border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-900 px-4 pt-3 pb-4">
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
Unscheduled
</h2>
<div className="space-y-2">
{unscheduledRoutines.map((r) => (
<div
key={r.id}
className="flex items-center gap-3 bg-gray-50 rounded-xl p-3"
className="flex items-center gap-3 bg-gray-50 dark:bg-gray-800 rounded-xl p-3"
>
<span className="text-xl flex-shrink-0">{r.icon || '✨'}</span>
<div className="flex-1 min-w-0">
<p className="font-medium text-gray-900 text-sm truncate">
<p className="font-medium text-gray-900 dark:text-gray-100 text-sm truncate">
{r.name}
</p>
{r.description && (
<p className="text-xs text-gray-500 truncate">
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
{r.description}
</p>
)}

View File

@@ -90,27 +90,27 @@ export default function SettingsPage() {
return (
<div className="p-4 space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-2xl font-bold text-gray-900">Settings</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Settings</h1>
{saved && (
<span className="text-sm text-green-600 animate-fade-in-up">Saved</span>
<span className="text-sm text-green-600 dark:text-green-400 animate-fade-in-up">Saved</span>
)}
</div>
{/* Session Experience */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Session Experience</h2>
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Session Experience</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm divide-y divide-gray-100 dark:divide-gray-700">
{/* Sound */}
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
{prefs.sound_enabled ? (
<VolumeIcon size={20} className="text-indigo-500" />
) : (
<VolumeOffIcon size={20} className="text-gray-400" />
<VolumeOffIcon size={20} className="text-gray-400 dark:text-gray-500" />
)}
<div>
<p className="font-medium text-gray-900">Sound effects</p>
<p className="text-sm text-gray-500">Subtle audio cues on step completion</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Sound effects</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Subtle audio cues on step completion</p>
</div>
</div>
<button
@@ -119,7 +119,7 @@ export default function SettingsPage() {
if (!prefs.sound_enabled) playStepComplete();
}}
className={`w-12 h-7 rounded-full transition-colors ${
prefs.sound_enabled ? 'bg-indigo-500' : 'bg-gray-300'
prefs.sound_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -131,10 +131,10 @@ export default function SettingsPage() {
{/* Haptics */}
<div className="flex items-center justify-between p-4">
<div className="flex items-center gap-3">
<SparklesIcon size={20} className={prefs.haptic_enabled ? 'text-indigo-500' : 'text-gray-400'} />
<SparklesIcon size={20} className={prefs.haptic_enabled ? 'text-indigo-500' : 'text-gray-400 dark:text-gray-500'} />
<div>
<p className="font-medium text-gray-900">Haptic feedback</p>
<p className="text-sm text-gray-500">Gentle vibration on actions</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Haptic feedback</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Gentle vibration on actions</p>
</div>
</div>
<button
@@ -143,7 +143,7 @@ export default function SettingsPage() {
if (!prefs.haptic_enabled) hapticTap();
}}
className={`w-12 h-7 rounded-full transition-colors ${
prefs.haptic_enabled ? 'bg-indigo-500' : 'bg-gray-300'
prefs.haptic_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -155,13 +155,13 @@ export default function SettingsPage() {
{/* Launch Screen */}
<div className="flex items-center justify-between p-4">
<div>
<p className="font-medium text-gray-900">Pre-routine launch screen</p>
<p className="text-sm text-gray-500">Environment check and emotion bridge</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Pre-routine launch screen</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Environment check and emotion bridge</p>
</div>
<button
onClick={() => updatePref('show_launch_screen', !prefs.show_launch_screen)}
className={`w-12 h-7 rounded-full transition-colors ${
prefs.show_launch_screen ? 'bg-indigo-500' : 'bg-gray-300'
prefs.show_launch_screen ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -174,8 +174,8 @@ export default function SettingsPage() {
{/* Notifications */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Notifications</h2>
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Notifications</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm divide-y divide-gray-100 dark:divide-gray-700">
{/* Push Notifications */}
<PushNotificationToggle />
@@ -183,13 +183,13 @@ export default function SettingsPage() {
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">ntfy</p>
<p className="text-sm text-gray-500">Push notifications via ntfy.sh</p>
<p className="font-medium text-gray-900 dark:text-gray-100">ntfy</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Push notifications via ntfy.sh</p>
</div>
<button
onClick={() => updateNotif({ ntfy_enabled: !notif.ntfy_enabled })}
className={`w-12 h-7 rounded-full transition-colors ${
notif.ntfy_enabled ? 'bg-indigo-500' : 'bg-gray-300'
notif.ntfy_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -204,7 +204,7 @@ export default function SettingsPage() {
value={notif.ntfy_topic}
onChange={(e) => setNotif({ ...notif, ntfy_topic: e.target.value })}
onBlur={() => updateNotif({ ntfy_topic: notif.ntfy_topic })}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400"
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-700"
/>
)}
</div>
@@ -213,13 +213,13 @@ export default function SettingsPage() {
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div>
<p className="font-medium text-gray-900">Discord</p>
<p className="text-sm text-gray-500">Get DMs from the Synculous bot</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Discord</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Get DMs from the Synculous bot</p>
</div>
<button
onClick={() => updateNotif({ discord_enabled: !notif.discord_enabled })}
className={`w-12 h-7 rounded-full transition-colors ${
notif.discord_enabled ? 'bg-indigo-500' : 'bg-gray-300'
notif.discord_enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
@@ -234,7 +234,7 @@ export default function SettingsPage() {
value={notif.discord_user_id}
onChange={(e) => setNotif({ ...notif, discord_user_id: e.target.value })}
onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })}
className="w-full px-3 py-2 text-sm border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 placeholder-gray-400"
className="w-full px-3 py-2 text-sm border border-gray-200 dark:border-gray-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-transparent text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 bg-white dark:bg-gray-700"
/>
)}
</div>
@@ -243,8 +243,8 @@ export default function SettingsPage() {
{/* Celebration Style */}
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Celebration Style</h2>
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Celebration Style</h2>
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm divide-y divide-gray-100 dark:divide-gray-700">
{[
{ value: 'standard', label: 'Standard', desc: 'Full animated celebration with stats and rewards' },
{ value: 'quick', label: 'Quick', desc: 'Brief confirmation, then back to dashboard' },
@@ -256,13 +256,13 @@ export default function SettingsPage() {
className="w-full flex items-center justify-between p-4 text-left"
>
<div>
<p className="font-medium text-gray-900">{option.label}</p>
<p className="text-sm text-gray-500">{option.desc}</p>
<p className="font-medium text-gray-900 dark:text-gray-100">{option.label}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">{option.desc}</p>
</div>
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
prefs.celebration_style === option.value
? 'border-indigo-500'
: 'border-gray-300'
: 'border-gray-300 dark:border-gray-600'
}`}>
{prefs.celebration_style === option.value && (
<div className="w-2.5 h-2.5 rounded-full bg-indigo-500" />

View File

@@ -122,7 +122,7 @@ export default function StatsPage() {
return (
<div className="p-4 space-y-6">
<h1 className="text-2xl font-bold text-gray-900">Your Progress</h1>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Your Progress</h1>
{/* Weekly Summary */}
{weeklySummary && (
@@ -148,11 +148,11 @@ export default function StatsPage() {
{/* Wins This Month */}
{victories.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Wins This Month</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Wins This Month</h2>
<div className="space-y-2">
{victories.map((victory, i) => (
<div key={i} className="bg-white rounded-xl p-4 shadow-sm flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-green-100 to-emerald-100 rounded-xl flex items-center justify-center">
<div key={i} className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm flex items-center gap-3">
<div className="w-10 h-10 bg-gradient-to-br from-green-100 to-emerald-100 dark:from-green-900/30 dark:to-emerald-900/30 rounded-xl flex items-center justify-center">
<span className="text-lg">
{victory.type === 'comeback' ? '💪' :
victory.type === 'weekend' ? '🎉' :
@@ -160,7 +160,7 @@ export default function StatsPage() {
victory.type === 'consistency' ? '🔥' : '⭐'}
</span>
</div>
<p className="text-sm text-gray-700 flex-1">{victory.message}</p>
<p className="text-sm text-gray-700 dark:text-gray-300 flex-1">{victory.message}</p>
</div>
))}
</div>
@@ -170,16 +170,16 @@ export default function StatsPage() {
{/* Consistency (formerly Streaks) */}
{streaks.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Your Consistency</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Your Consistency</h2>
<div className="space-y-2">
{streaks.map((streak) => (
<div key={streak.routine_id} className="bg-white rounded-xl p-4 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-orange-100 to-red-100 rounded-xl flex items-center justify-center">
<div key={streak.routine_id} className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm flex items-center gap-4">
<div className="w-12 h-12 bg-gradient-to-br from-orange-100 to-red-100 dark:from-orange-900/30 dark:to-red-900/30 rounded-xl flex items-center justify-center">
<FlameIcon className="text-orange-500" size={24} />
</div>
<div className="flex-1">
<p className="font-medium text-gray-900">{streak.routine_name}</p>
<p className="text-sm text-gray-500">
<p className="font-medium text-gray-900 dark:text-gray-100">{streak.routine_name}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">
{getStreakMessage(streak.current_streak)}
</p>
</div>
@@ -187,17 +187,17 @@ export default function StatsPage() {
{streak.current_streak > 0 ? (
<>
<p className="text-2xl font-bold text-orange-500">{streak.current_streak}</p>
<p className="text-xs text-gray-500">days</p>
<p className="text-xs text-gray-500 dark:text-gray-400">days</p>
</>
) : (
<p className="text-sm text-gray-400">Ready</p>
<p className="text-sm text-gray-400 dark:text-gray-500">Ready</p>
)}
</div>
</div>
))}
{/* Longest streak callout */}
{streaks.some(s => s.longest_streak > 0) && (
<p className="text-sm text-gray-400 text-center mt-2">
<p className="text-sm text-gray-400 dark:text-gray-500 text-center mt-2">
Your personal best: {Math.max(...streaks.map(s => s.longest_streak))} days you&apos;ve done it before
</p>
)}
@@ -208,11 +208,11 @@ export default function StatsPage() {
{/* Per-Routine Stats */}
{routines.length > 0 && (
<div>
<h2 className="text-lg font-semibold text-gray-900 mb-3">Routine Details</h2>
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Routine Details</h2>
<select
value={selectedRoutine}
onChange={(e) => setSelectedRoutine(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-xl bg-white mb-4"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-xl bg-white dark:bg-gray-800 text-gray-900 dark:text-gray-100 mb-4"
>
{routines.map((routine) => (
<option key={routine.id} value={routine.id}>
@@ -223,34 +223,34 @@ export default function StatsPage() {
{routineStats && (
<div className="grid grid-cols-2 gap-3">
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<TargetIcon className="text-indigo-500 mb-2" size={24} />
<p className={`text-lg font-bold ${getCompletionLabel(routineStats.completion_rate_percent).color}`}>
{getCompletionLabel(routineStats.completion_rate_percent).label}
</p>
<p className="text-sm text-gray-400">{routineStats.completion_rate_percent}%</p>
<p className="text-sm text-gray-400 dark:text-gray-500">{routineStats.completion_rate_percent}%</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<ClockIcon className="text-purple-500 mb-2" size={24} />
<p className="text-2xl font-bold text-gray-900">{formatTime(routineStats.avg_duration_minutes)}</p>
<p className="text-sm text-gray-500">Avg Duration</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">{formatTime(routineStats.avg_duration_minutes)}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Avg Duration</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<StarIcon className="text-green-500 mb-2" size={24} />
<p className="text-2xl font-bold text-gray-900">{routineStats.completed}</p>
<p className="text-sm text-gray-500">Completed</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">{routineStats.completed}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Completed</p>
</div>
<div className="bg-white rounded-xl p-4 shadow-sm">
<div className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm">
<ActivityIcon className="text-pink-500 mb-2" size={24} />
<p className="text-2xl font-bold text-gray-900">{routineStats.total_sessions}</p>
<p className="text-sm text-gray-500">Total Sessions</p>
<p className="text-2xl font-bold text-gray-900 dark:text-gray-100">{routineStats.total_sessions}</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Total Sessions</p>
</div>
</div>
)}
{/* Plateau messaging */}
{routineStats && routineStats.total_sessions >= 5 && (
<p className="text-sm text-gray-400 text-center mt-3">
<p className="text-sm text-gray-400 dark:text-gray-500 text-center mt-3">
{routineStats.completion_rate_percent >= 60
? "You're showing up consistently — that's the hard part. The exact rate doesn't matter."
: "Life has seasons. The fact that you're checking in shows this matters to you."}

View File

@@ -54,34 +54,34 @@ export default function TemplatesPage() {
return (
<div className="p-4 space-y-4">
<h1 className="text-2xl font-bold text-gray-900">Templates</h1>
<p className="text-gray-500">Start with a pre-made routine</p>
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Templates</h1>
<p className="text-gray-500 dark:text-gray-400">Start with a pre-made routine</p>
{templates.length === 0 ? (
<div className="bg-white rounded-xl p-8 shadow-sm text-center mt-4">
<div className="w-16 h-16 bg-gray-100 rounded-full flex items-center justify-center mx-auto mb-4">
<FlameIcon className="text-gray-400" size={32} />
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center mt-4">
<div className="w-16 h-16 bg-gray-100 dark:bg-gray-700 rounded-full flex items-center justify-center mx-auto mb-4">
<FlameIcon className="text-gray-400 dark:text-gray-500" size={32} />
</div>
<h3 className="font-semibold text-gray-900 mb-1">No templates yet</h3>
<p className="text-gray-500 text-sm">Templates will appear here when available</p>
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No templates yet</h3>
<p className="text-gray-500 dark:text-gray-400 text-sm">Templates will appear here when available</p>
</div>
) : (
<div className="grid gap-4">
{templates.map((template) => (
<div
key={template.id}
className="bg-white rounded-xl p-4 shadow-sm"
className="bg-white dark:bg-gray-800 rounded-xl p-4 shadow-sm"
>
<div className="flex items-center gap-4">
<div className="w-14 h-14 bg-gradient-to-br from-indigo-100 to-pink-100 rounded-xl flex items-center justify-center flex-shrink-0">
<div className="w-14 h-14 bg-gradient-to-br from-indigo-100 to-pink-100 dark:from-indigo-900/50 dark:to-pink-900/50 rounded-xl flex items-center justify-center flex-shrink-0">
<span className="text-3xl">{template.icon || '✨'}</span>
</div>
<div className="flex-1 min-w-0">
<h3 className="font-semibold text-gray-900">{template.name}</h3>
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{template.name}</h3>
{template.description && (
<p className="text-gray-500 text-sm truncate">{template.description}</p>
<p className="text-gray-500 dark:text-gray-400 text-sm truncate">{template.description}</p>
)}
<p className="text-gray-400 text-xs mt-1">{template.step_count} steps</p>
<p className="text-gray-400 dark:text-gray-500 text-xs mt-1">{template.step_count} steps</p>
</div>
<button
onClick={() => handleClone(template.id)}

View File

@@ -1,10 +1,17 @@
@import "tailwindcss";
@variant dark (&:where(.dark, .dark *));
:root {
--background: #ffffff;
--foreground: #171717;
}
.dark {
--background: #0a0a0a;
--foreground: #ededed;
}
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
@@ -12,13 +19,6 @@
--font-mono: var(--font-geist-mono);
}
@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}
body {
background: var(--background);
color: var(--foreground);

View File

@@ -1,6 +1,7 @@
import type { Metadata } from "next";
import "./globals.css";
import { AuthProvider } from "@/components/auth/AuthProvider";
import { ThemeProvider } from "@/components/theme/ThemeProvider";
export const metadata: Metadata = {
title: "Synculous",
@@ -31,9 +32,16 @@ export default function RootLayout({
<link rel="apple-touch-icon" href="/icon-192.png" />
</head>
<body className="antialiased">
<AuthProvider>
{children}
</AuthProvider>
<script
dangerouslySetInnerHTML={{
__html: `(function(){var t=localStorage.getItem('theme');if(t==='dark'||(!t&&window.matchMedia('(prefers-color-scheme: dark)').matches))document.documentElement.classList.add('dark')})()`,
}}
/>
<ThemeProvider>
<AuthProvider>
{children}
</AuthProvider>
</ThemeProvider>
<script
dangerouslySetInnerHTML={{
__html: `if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js')}`,

View File

@@ -36,47 +36,47 @@ export default function LoginPage() {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-indigo-500 via-purple-500 to-pink-500 p-4">
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-md p-8">
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-2xl w-full max-w-md p-8">
<div className="flex flex-col items-center mb-8">
<div className="w-16 h-16 bg-gradient-to-br from-indigo-500 to-pink-500 rounded-2xl flex items-center justify-center mb-4">
<HeartIcon className="text-white" size={32} />
</div>
<h1 className="text-2xl font-bold text-gray-900">Synculous</h1>
<p className="text-gray-500 mt-1">
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Synculous</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
{isLogin ? 'Welcome back!' : 'Create your account'}
</p>
</div>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="bg-red-50 text-red-600 px-4 py-3 rounded-lg text-sm">
<div className="bg-red-50 dark:bg-red-900/30 text-red-600 dark:text-red-400 px-4 py-3 rounded-lg text-sm">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Username
</label>
<input
type="text"
value={username}
onChange={(e) => setUsername(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition"
placeholder="Enter your username"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Password
</label>
<input
type="password"
value={password}
onChange={(e) => setPassword(e.target.value)}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition"
className="w-full px-4 py-3 border border-gray-300 dark:border-gray-600 rounded-lg bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 placeholder-gray-400 dark:placeholder-gray-500 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none transition"
placeholder="Enter your password"
required
/>

View File

@@ -83,14 +83,14 @@ export default function PushNotificationToggle() {
return (
<div className="flex items-center justify-between p-4">
<div>
<p className="font-medium text-gray-900">Push notifications</p>
<p className="text-sm text-gray-500">Get reminders on this device</p>
<p className="font-medium text-gray-900 dark:text-gray-100">Push notifications</p>
<p className="text-sm text-gray-500 dark:text-gray-400">Get reminders on this device</p>
</div>
<button
onClick={toggle}
disabled={loading}
className={`w-12 h-7 rounded-full transition-colors ${
enabled ? 'bg-indigo-500' : 'bg-gray-300'
enabled ? 'bg-indigo-500' : 'bg-gray-300 dark:bg-gray-600'
} ${loading ? 'opacity-50' : ''}`}
>
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${

View File

@@ -0,0 +1,52 @@
'use client';
import React, { createContext, useContext, useState, useEffect } from 'react';
interface ThemeContextType {
isDark: boolean;
toggleDark: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
export function ThemeProvider({ children }: { children: React.ReactNode }) {
const [isDark, setIsDark] = useState(false);
useEffect(() => {
const stored = localStorage.getItem('theme');
const prefersDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
const dark = stored === 'dark' || (!stored && prefersDark);
setIsDark(dark);
if (dark) {
document.documentElement.classList.add('dark');
}
}, []);
const toggleDark = () => {
setIsDark((prev) => {
const next = !prev;
if (next) {
document.documentElement.classList.add('dark');
localStorage.setItem('theme', 'dark');
} else {
document.documentElement.classList.remove('dark');
localStorage.setItem('theme', 'light');
}
return next;
});
};
return (
<ThemeContext.Provider value={{ isDark, toggleDark }}>
{children}
</ThemeContext.Provider>
);
}
export function useTheme() {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
}