added dark mode
This commit is contained in:
106
synculous-client/darkmode.md
Normal file
106
synculous-client/darkmode.md
Normal 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
|
||||||
@@ -95,13 +95,13 @@ export default function HistoryPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-4">
|
<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 */}
|
{/* Filter */}
|
||||||
<select
|
<select
|
||||||
value={selectedRoutine}
|
value={selectedRoutine}
|
||||||
onChange={(e) => setSelectedRoutine(e.target.value)}
|
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>
|
<option value="all">All Routines</option>
|
||||||
{routines.map((routine) => (
|
{routines.map((routine) => (
|
||||||
@@ -112,39 +112,39 @@ export default function HistoryPage() {
|
|||||||
</select>
|
</select>
|
||||||
|
|
||||||
{history.length === 0 ? (
|
{history.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
|
||||||
<CalendarIcon className="text-gray-400 mx-auto mb-4" size={40} />
|
<CalendarIcon className="text-gray-400 dark:text-gray-500 mx-auto mb-4" size={40} />
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">No history yet</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No history yet</h3>
|
||||||
<p className="text-gray-500 text-sm">Complete a routine to see it here</p>
|
<p className="text-gray-500 dark:text-gray-400 text-sm">Complete a routine to see it here</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3">
|
||||||
{history.map((session) => (
|
{history.map((session) => (
|
||||||
<div
|
<div
|
||||||
key={session.id}
|
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={`
|
<div className={`
|
||||||
w-10 h-10 rounded-full flex items-center justify-center
|
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' ? (
|
{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>
|
||||||
<div className="flex-1">
|
<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'}
|
{(session as any).routine_name || 'Routine'}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{formatDate(session.created_at)}
|
{formatDate(session.created_at)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<span className={`
|
<span className={`
|
||||||
text-xs font-medium px-2 py-1 rounded-full
|
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}
|
{session.status}
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
import { useRouter, usePathname } from 'next/navigation';
|
import { useRouter, usePathname } from 'next/navigation';
|
||||||
import { useAuth } from '@/components/auth/AuthProvider';
|
import { useAuth } from '@/components/auth/AuthProvider';
|
||||||
|
import { useTheme } from '@/components/theme/ThemeProvider';
|
||||||
import api from '@/lib/api';
|
import api from '@/lib/api';
|
||||||
import {
|
import {
|
||||||
HomeIcon,
|
HomeIcon,
|
||||||
@@ -13,7 +14,9 @@ import {
|
|||||||
SettingsIcon,
|
SettingsIcon,
|
||||||
LogOutIcon,
|
LogOutIcon,
|
||||||
CopyIcon,
|
CopyIcon,
|
||||||
HeartIcon
|
HeartIcon,
|
||||||
|
SunIcon,
|
||||||
|
MoonIcon,
|
||||||
} from '@/components/ui/Icons';
|
} from '@/components/ui/Icons';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
|
|
||||||
@@ -32,6 +35,7 @@ export default function DashboardLayout({
|
|||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}) {
|
}) {
|
||||||
const { isAuthenticated, isLoading, logout } = useAuth();
|
const { isAuthenticated, isLoading, logout } = useAuth();
|
||||||
|
const { isDark, toggleDark } = useTheme();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const pathname = usePathname();
|
const pathname = usePathname();
|
||||||
|
|
||||||
@@ -57,7 +61,7 @@ export default function DashboardLayout({
|
|||||||
<div className="min-h-screen flex items-center justify-center">
|
<div className="min-h-screen flex items-center justify-center">
|
||||||
<div className="animate-pulse flex flex-col items-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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -75,22 +79,29 @@ export default function DashboardLayout({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
<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 justify-between px-4 py-3">
|
||||||
<div className="flex items-center gap-2">
|
<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">
|
<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} />
|
<HeartIcon className="text-white" size={16} />
|
||||||
</div>
|
</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>
|
||||||
<div className="flex items-center gap-2">
|
<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} />
|
<SettingsIcon size={20} />
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={logout}
|
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} />
|
<LogOutIcon size={20} />
|
||||||
</button>
|
</button>
|
||||||
@@ -102,7 +113,7 @@ export default function DashboardLayout({
|
|||||||
{children}
|
{children}
|
||||||
</main>
|
</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">
|
<div className="flex justify-around py-2">
|
||||||
{navItems.map((item) => {
|
{navItems.map((item) => {
|
||||||
const isActive = pathname === item.href ||
|
const isActive = pathname === item.href ||
|
||||||
@@ -113,8 +124,8 @@ export default function DashboardLayout({
|
|||||||
href={item.href}
|
href={item.href}
|
||||||
className={`flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-colors ${
|
className={`flex flex-col items-center gap-1 px-3 py-2 rounded-lg transition-colors ${
|
||||||
isActive
|
isActive
|
||||||
? 'text-indigo-600'
|
? 'text-indigo-600 dark:text-indigo-400'
|
||||||
: 'text-gray-500 hover:text-gray-700'
|
: 'text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
<item.icon size={20} />
|
<item.icon size={20} />
|
||||||
|
|||||||
@@ -81,52 +81,52 @@ export default function NewMedicationPage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
<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">
|
<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} />
|
<ArrowLeftIcon size={24} />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-4 space-y-6">
|
<form onSubmit={handleSubmit} className="p-4 space-y-6">
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</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>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="e.g., Vitamin D"
|
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>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 gap-4">
|
<div className="grid grid-cols-2 gap-4">
|
||||||
<div>
|
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={dosage}
|
value={dosage}
|
||||||
onChange={(e) => setDosage(e.target.value)}
|
onChange={(e) => setDosage(e.target.value)}
|
||||||
placeholder="e.g., 1000"
|
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>
|
||||||
<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
|
<select
|
||||||
value={unit}
|
value={unit}
|
||||||
onChange={(e) => setUnit(e.target.value)}
|
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="mg">mg</option>
|
||||||
<option value="mcg">mcg</option>
|
<option value="mcg">mcg</option>
|
||||||
@@ -140,11 +140,11 @@ export default function NewMedicationPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<select
|
||||||
value={frequency}
|
value={frequency}
|
||||||
onChange={(e) => setFrequency(e.target.value)}
|
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="daily">Daily</option>
|
||||||
<option value="specific_days">Specific Days of Week</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 */}
|
{/* Day-of-week picker for specific_days */}
|
||||||
{frequency === 'specific_days' && (
|
{frequency === 'specific_days' && (
|
||||||
<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">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{DAY_OPTIONS.map(({ value, label }) => (
|
{DAY_OPTIONS.map(({ value, label }) => (
|
||||||
<button
|
<button
|
||||||
@@ -166,7 +166,7 @@ export default function NewMedicationPage() {
|
|||||||
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
||||||
daysOfWeek.includes(value)
|
daysOfWeek.includes(value)
|
||||||
? 'bg-indigo-600 text-white border-indigo-600'
|
? '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}
|
{label}
|
||||||
@@ -180,22 +180,22 @@ export default function NewMedicationPage() {
|
|||||||
{frequency === 'every_n_days' && (
|
{frequency === 'every_n_days' && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<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
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={intervalDays}
|
value={intervalDays}
|
||||||
onChange={(e) => setIntervalDays(parseInt(e.target.value) || 1)}
|
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>
|
||||||
<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
|
<input
|
||||||
type="date"
|
type="date"
|
||||||
value={startDate}
|
value={startDate}
|
||||||
onChange={(e) => setStartDate(e.target.value)}
|
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>
|
||||||
</div>
|
</div>
|
||||||
@@ -205,17 +205,17 @@ export default function NewMedicationPage() {
|
|||||||
{frequency !== 'as_needed' && (
|
{frequency !== 'as_needed' && (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddTime}
|
onClick={handleAddTime}
|
||||||
className="text-indigo-600 text-sm font-medium"
|
className="text-indigo-600 dark:text-indigo-400 text-sm font-medium"
|
||||||
>
|
>
|
||||||
+ Add Time
|
+ Add Time
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{frequency === 'daily' && (
|
{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">
|
<div className="space-y-2">
|
||||||
{times.map((time, index) => (
|
{times.map((time, index) => (
|
||||||
@@ -224,13 +224,13 @@ export default function NewMedicationPage() {
|
|||||||
type="time"
|
type="time"
|
||||||
value={time}
|
value={time}
|
||||||
onChange={(e) => handleTimeChange(index, e.target.value)}
|
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 && (
|
{times.length > 1 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleRemoveTime(index)}
|
onClick={() => handleRemoveTime(index)}
|
||||||
className="text-red-500 px-3"
|
className="text-red-500 dark:text-red-400 px-3"
|
||||||
>
|
>
|
||||||
Remove
|
Remove
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -214,7 +214,7 @@ export default function MedicationsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<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">
|
<Link href="/dashboard/medications/new" className="bg-indigo-600 text-white p-2 rounded-full">
|
||||||
<PlusIcon size={24} />
|
<PlusIcon size={24} />
|
||||||
</Link>
|
</Link>
|
||||||
@@ -224,7 +224,7 @@ export default function MedicationsPage() {
|
|||||||
<PushNotificationToggle />
|
<PushNotificationToggle />
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -232,38 +232,38 @@ export default function MedicationsPage() {
|
|||||||
{/* Due Now Section */}
|
{/* Due Now Section */}
|
||||||
{dueEntries.length > 0 && (
|
{dueEntries.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="space-y-3">
|
||||||
{dueEntries.map((entry) => (
|
{dueEntries.map((entry) => (
|
||||||
<div
|
<div
|
||||||
key={`${entry.item.medication.id}-${entry.time}`}
|
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 className="flex items-center justify-between mb-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<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 && (
|
{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>
|
</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>
|
</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">
|
<div className="flex items-center gap-2">
|
||||||
<ClockIcon size={16} className="text-gray-500" />
|
<ClockIcon size={16} className="text-gray-500 dark:text-gray-400" />
|
||||||
<span className="font-medium">{entry.time}</span>
|
<span className="font-medium text-gray-900 dark:text-gray-100">{entry.time}</span>
|
||||||
{entry.status === 'overdue' && (
|
{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>
|
</div>
|
||||||
{entry.status === 'taken' ? (
|
{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
|
<CheckIcon size={16} /> Taken
|
||||||
</span>
|
</span>
|
||||||
) : entry.status === 'skipped' ? (
|
) : 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">
|
<div className="flex gap-2">
|
||||||
<button
|
<button
|
||||||
@@ -274,7 +274,7 @@ export default function MedicationsPage() {
|
|||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleSkip(entry.item.medication.id, entry.time)}
|
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
|
Skip
|
||||||
</button>
|
</button>
|
||||||
@@ -290,18 +290,18 @@ export default function MedicationsPage() {
|
|||||||
{/* PRN Section */}
|
{/* PRN Section */}
|
||||||
{prnEntries.length > 0 && (
|
{prnEntries.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="space-y-3">
|
||||||
{prnEntries.map((item) => (
|
{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 className="flex items-center justify-between mb-2">
|
||||||
<div>
|
<div>
|
||||||
<h3 className="font-semibold text-gray-900">{item.medication.name}</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100">{item.medication.name}</h3>
|
||||||
<p className="text-sm text-gray-500">{item.medication.dosage} {item.medication.unit}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">{item.medication.dosage} {item.medication.unit}</p>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
||||||
<span className="text-gray-500 text-sm">As needed</span>
|
<span className="text-gray-500 dark:text-gray-400 text-sm">As needed</span>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleTake(item.medication.id)}
|
onClick={() => handleTake(item.medication.id)}
|
||||||
className="bg-green-600 text-white px-3 py-1 rounded-lg text-sm font-medium"
|
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 */}
|
{/* Upcoming Section */}
|
||||||
{upcomingEntries.length > 0 && (
|
{upcomingEntries.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="space-y-3">
|
||||||
{upcomingEntries.map((entry) => (
|
{upcomingEntries.map((entry) => (
|
||||||
<div
|
<div
|
||||||
key={`${entry.item.medication.id}-${entry.time}`}
|
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 className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center gap-2">
|
<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 && (
|
{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>
|
</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>
|
||||||
<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} />
|
<ClockIcon size={16} />
|
||||||
<span className="font-medium">{entry.time}</span>
|
<span className="font-medium">{entry.time}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -348,15 +348,15 @@ export default function MedicationsPage() {
|
|||||||
|
|
||||||
{/* All Medications */}
|
{/* All Medications */}
|
||||||
<div>
|
<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 ? (
|
{medications.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
|
<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 rounded-full flex items-center justify-center mx-auto mb-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">
|
||||||
<PillIcon className="text-gray-400" size={32} />
|
<PillIcon className="text-gray-400 dark:text-gray-500" size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">No medications yet</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No medications yet</h3>
|
||||||
<p className="text-gray-500 text-sm mb-4">Add your medications to track them</p>
|
<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">
|
<Link href="/dashboard/medications/new" className="inline-block bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium">
|
||||||
Add Medication
|
Add Medication
|
||||||
</Link>
|
</Link>
|
||||||
@@ -366,41 +366,41 @@ export default function MedicationsPage() {
|
|||||||
{medications.map((med) => {
|
{medications.map((med) => {
|
||||||
const { percent: adherencePercent, isPrn } = getAdherenceForMed(med.id);
|
const { percent: adherencePercent, isPrn } = getAdherenceForMed(med.id);
|
||||||
return (
|
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 items-start justify-between">
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<div className="flex items-center gap-2">
|
<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 && (
|
{!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>
|
</div>
|
||||||
<p className="text-gray-500 text-sm">{med.dosage} {med.unit} · {formatSchedule(med)}</p>
|
<p className="text-gray-500 dark:text-gray-400 text-sm">{med.dosage} {med.unit} · {formatSchedule(med)}</p>
|
||||||
{med.times.length > 0 && (
|
{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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDelete(med.id)}
|
onClick={() => handleDelete(med.id)}
|
||||||
className="text-red-500 p-2"
|
className="text-red-500 dark:text-red-400 p-2"
|
||||||
>
|
>
|
||||||
<TrashIcon size={18} />
|
<TrashIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Adherence */}
|
{/* 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 ? (
|
{isPrn || adherencePercent === null ? (
|
||||||
<span className="text-sm text-gray-400">PRN — no adherence tracking</span>
|
<span className="text-sm text-gray-400 dark:text-gray-500">PRN — no adherence tracking</span>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-center justify-between mb-1">
|
<div className="flex items-center justify-between mb-1">
|
||||||
<span className="text-sm text-gray-500">30-day adherence</span>
|
<span className="text-sm text-gray-500 dark:text-gray-400">30-day adherence</span>
|
||||||
<span className={`font-semibold ${adherencePercent >= 80 ? 'text-green-600' : adherencePercent >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
|
<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}%
|
{adherencePercent}%
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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
|
<div
|
||||||
className={`h-full ${adherencePercent >= 80 ? 'bg-green-500' : adherencePercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
className={`h-full ${adherencePercent >= 80 ? 'bg-green-500' : adherencePercent >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||||
style={{ width: `${adherencePercent}%` }}
|
style={{ width: `${adherencePercent}%` }}
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ export default function DashboardPage() {
|
|||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
{/* Active Session Banner */}
|
{/* Active Session Banner */}
|
||||||
{activeSession && activeSession.session.status === 'active' && (
|
{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 className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-white/80 text-sm">Continue where you left off</p>
|
<p className="text-white/80 text-sm">Continue where you left off</p>
|
||||||
@@ -162,8 +162,8 @@ export default function DashboardPage() {
|
|||||||
|
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">{getGreeting()}, {user?.username}!</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">{getGreeting()}, {user?.username}!</h1>
|
||||||
<p className="text-gray-500 mt-1">Let's build some great habits today.</p>
|
<p className="text-gray-500 dark:text-gray-400 mt-1">Let's build some great habits today.</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* "Never miss twice" Recovery Cards */}
|
{/* "Never miss twice" Recovery Cards */}
|
||||||
@@ -171,11 +171,11 @@ export default function DashboardPage() {
|
|||||||
const routine = routines.find(r => r.id === recovery.routine_id);
|
const routine = routines.find(r => r.id === recovery.routine_id);
|
||||||
if (!routine) return null;
|
if (!routine) return null;
|
||||||
return (
|
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 className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="text-amber-800 text-sm font-medium mb-1">Welcome back</p>
|
<p className="text-amber-800 dark:text-amber-200 text-sm font-medium mb-1">Welcome back</p>
|
||||||
<p className="text-amber-700 text-sm">
|
<p className="text-amber-700 dark:text-amber-300 text-sm">
|
||||||
It's been a couple days since {routine.icon} {routine.name}. That's completely okay — picking it back up today is what matters most.
|
It's been a couple days since {routine.icon} {routine.name}. That's completely okay — picking it back up today is what matters most.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -193,44 +193,44 @@ export default function DashboardPage() {
|
|||||||
{/* Weekly Stats — identity-based language */}
|
{/* Weekly Stats — identity-based language */}
|
||||||
{weeklySummary && weeklySummary.total_completed > 0 && (
|
{weeklySummary && weeklySummary.total_completed > 0 && (
|
||||||
<div className="grid grid-cols-3 gap-3">
|
<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">
|
<div className="flex items-center gap-2 text-indigo-600 mb-1">
|
||||||
<StarIcon size={18} />
|
<StarIcon size={18} />
|
||||||
<span className="text-xs font-medium">Done</span>
|
<span className="text-xs font-medium">Done</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-gray-900">{weeklySummary.total_completed}</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 mt-0.5">this week</p>
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">this week</p>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-2 text-purple-600 mb-1">
|
||||||
<ClockIcon size={18} />
|
<ClockIcon size={18} />
|
||||||
<span className="text-xs font-medium">Invested</span>
|
<span className="text-xs font-medium">Invested</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-gray-900">{formatTime(weeklySummary.total_time_minutes)}</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 mt-0.5">in yourself</p>
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">in yourself</p>
|
||||||
</div>
|
</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">
|
<div className="flex items-center gap-2 text-pink-600 mb-1">
|
||||||
<ActivityIcon size={18} />
|
<ActivityIcon size={18} />
|
||||||
<span className="text-xs font-medium">Active</span>
|
<span className="text-xs font-medium">Active</span>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-2xl font-bold text-gray-900">{weeklySummary.routines_started}</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 mt-0.5">routines</p>
|
<p className="text-xs text-gray-400 dark:text-gray-500 mt-0.5">routines</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Quick Start Routines */}
|
{/* Quick Start Routines */}
|
||||||
<div>
|
<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 ? (
|
{routines.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
|
<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 rounded-full flex items-center justify-center mx-auto mb-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" size={32} />
|
<FlameIcon className="text-gray-400 dark:text-gray-500" size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">No routines yet</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No routines yet</h3>
|
||||||
<p className="text-gray-500 text-sm mb-4">Create your first routine to get started</p>
|
<p className="text-gray-500 dark:text-gray-400 text-sm mb-4">Create your first routine to get started</p>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/routines/new"
|
href="/dashboard/routines/new"
|
||||||
className="inline-block bg-indigo-600 text-white px-4 py-2 rounded-lg font-medium"
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
key={routine.id}
|
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="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>
|
<span className="text-2xl">{routine.icon || '✨'}</span>
|
||||||
</div>
|
</div>
|
||||||
<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 ? (
|
{streak && streak.current_streak > 0 ? (
|
||||||
<p className="text-sm text-orange-500 flex items-center gap-1">
|
<p className="text-sm text-orange-500 flex items-center gap-1">
|
||||||
<FlameIcon size={14} />
|
<FlameIcon size={14} />
|
||||||
{streak.current_streak} day{streak.current_streak !== 1 ? 's' : ''}
|
{streak.current_streak} day{streak.current_streak !== 1 ? 's' : ''}
|
||||||
</p>
|
</p>
|
||||||
) : routine.description ? (
|
) : 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}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -293,7 +293,7 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -105,24 +105,24 @@ export default function LaunchScreen() {
|
|||||||
if (!routine) return null;
|
if (!routine) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
<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">
|
<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} />
|
<ArrowLeftIcon size={24} />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
{/* Routine info */}
|
{/* Routine info */}
|
||||||
<div className="text-center py-4">
|
<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>
|
<span className="text-5xl">{routine.icon || '✨'}</span>
|
||||||
</div>
|
</div>
|
||||||
<h2 className="text-2xl font-bold text-gray-900">{routine.name}</h2>
|
<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 mt-2">
|
<div className="flex items-center justify-center gap-2 text-gray-500 dark:text-gray-400 mt-2">
|
||||||
<ClockIcon size={16} />
|
<ClockIcon size={16} />
|
||||||
<span>~{totalDuration} minutes</span>
|
<span>~{totalDuration} minutes</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
@@ -132,15 +132,15 @@ export default function LaunchScreen() {
|
|||||||
|
|
||||||
{/* Implementation Intention */}
|
{/* Implementation Intention */}
|
||||||
{(routine.habit_stack_after || schedule) && (
|
{(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 && (
|
{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{' '}
|
After <span className="font-semibold">{routine.habit_stack_after}</span>, I will start{' '}
|
||||||
<span className="font-semibold">{routine.name}</span>
|
<span className="font-semibold">{routine.name}</span>
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{schedule && (
|
{schedule && (
|
||||||
<p className="text-indigo-700 text-sm">
|
<p className="text-indigo-700 dark:text-indigo-300 text-sm">
|
||||||
{schedule.days.length > 0 && (
|
{schedule.days.length > 0 && (
|
||||||
<span className="font-semibold">
|
<span className="font-semibold">
|
||||||
{schedule.days.length === 7 ? 'Every day' :
|
{schedule.days.length === 7 ? 'Every day' :
|
||||||
@@ -161,8 +161,8 @@ export default function LaunchScreen() {
|
|||||||
|
|
||||||
{/* Environment Check */}
|
{/* Environment Check */}
|
||||||
{envPrompts.length > 0 && (
|
{envPrompts.length > 0 && (
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
|
||||||
<h3 className="font-semibold text-gray-900 mb-3">Quick check</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-3">Quick check</h3>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{envPrompts.map((prompt, i) => (
|
{envPrompts.map((prompt, i) => (
|
||||||
<button
|
<button
|
||||||
@@ -170,18 +170,18 @@ export default function LaunchScreen() {
|
|||||||
onClick={() => toggleEnvironmentCheck(i)}
|
onClick={() => toggleEnvironmentCheck(i)}
|
||||||
className={`w-full flex items-center gap-3 p-3 rounded-xl transition ${
|
className={`w-full flex items-center gap-3 p-3 rounded-xl transition ${
|
||||||
environmentChecked.has(i)
|
environmentChecked.has(i)
|
||||||
? 'bg-green-50 border border-green-200'
|
? 'bg-green-50 dark:bg-green-900/30 border border-green-200 dark:border-green-800'
|
||||||
: 'bg-gray-50 border border-gray-200'
|
: '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 ${
|
<div className={`w-6 h-6 rounded-full border-2 flex items-center justify-center ${
|
||||||
environmentChecked.has(i)
|
environmentChecked.has(i)
|
||||||
? 'border-green-500 bg-green-500'
|
? '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" />}
|
{environmentChecked.has(i) && <CheckIcon size={14} className="text-white" />}
|
||||||
</div>
|
</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}
|
{prompt}
|
||||||
</span>
|
</span>
|
||||||
</button>
|
</button>
|
||||||
@@ -191,11 +191,11 @@ export default function LaunchScreen() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Emotion Bridge */}
|
{/* Emotion Bridge */}
|
||||||
<div className="bg-white rounded-2xl p-4 shadow-sm">
|
<div className="bg-white dark:bg-gray-800 rounded-2xl p-4 shadow-sm">
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">
|
||||||
When you finish in ~{totalDuration} minutes, how will you feel?
|
When you finish in ~{totalDuration} minutes, how will you feel?
|
||||||
</h3>
|
</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">
|
<div className="grid grid-cols-2 gap-2">
|
||||||
{EMOTION_OPTIONS.map((option) => (
|
{EMOTION_OPTIONS.map((option) => (
|
||||||
<button
|
<button
|
||||||
@@ -203,12 +203,12 @@ export default function LaunchScreen() {
|
|||||||
onClick={() => setSelectedEmotion(option.label)}
|
onClick={() => setSelectedEmotion(option.label)}
|
||||||
className={`flex items-center gap-2 p-3 rounded-xl border transition ${
|
className={`flex items-center gap-2 p-3 rounded-xl border transition ${
|
||||||
selectedEmotion === option.label
|
selectedEmotion === option.label
|
||||||
? 'border-indigo-500 bg-indigo-50'
|
? 'border-indigo-500 bg-indigo-50 dark:bg-indigo-900/30'
|
||||||
: 'border-gray-200 hover:border-gray-300'
|
: '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-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>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -241,14 +241,14 @@ export default function RoutineDetailPage() {
|
|||||||
if (!routine) return null;
|
if (!routine) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
<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 justify-between px-4 py-3">
|
||||||
<div className="flex items-center gap-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} />
|
<ArrowLeftIcon size={24} />
|
||||||
</button>
|
</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}
|
{isEditing ? 'Edit Routine' : routine.name}
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,13 +268,13 @@ export default function RoutineDetailPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
disabled={isDeleting}
|
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'}
|
{isDeleting ? 'Deleting...' : 'Delete'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsEditing(true)}
|
onClick={() => setIsEditing(true)}
|
||||||
className="text-indigo-600 font-medium"
|
className="text-indigo-600 dark:text-indigo-400 font-medium"
|
||||||
>
|
>
|
||||||
Edit
|
Edit
|
||||||
</button>
|
</button>
|
||||||
@@ -286,9 +286,9 @@ export default function RoutineDetailPage() {
|
|||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
{isEditing ? (
|
{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>
|
<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">
|
<div className="flex flex-wrap gap-2">
|
||||||
{ICONS.map((i) => (
|
{ICONS.map((i) => (
|
||||||
<button
|
<button
|
||||||
@@ -297,8 +297,8 @@ export default function RoutineDetailPage() {
|
|||||||
onClick={() => setEditIcon(i)}
|
onClick={() => setEditIcon(i)}
|
||||||
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition ${
|
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition ${
|
||||||
editIcon === i
|
editIcon === i
|
||||||
? 'bg-indigo-100 ring-2 ring-indigo-600'
|
? 'bg-indigo-100 dark:bg-indigo-900/50 ring-2 ring-indigo-600'
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i}
|
{i}
|
||||||
@@ -307,53 +307,53 @@ export default function RoutineDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editName}
|
value={editName}
|
||||||
onChange={(e) => setEditName(e.target.value)}
|
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>
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editDescription}
|
value={editDescription}
|
||||||
onChange={(e) => setEditDescription(e.target.value)}
|
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>
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editLocation}
|
value={editLocation}
|
||||||
onChange={(e) => setEditLocation(e.target.value)}
|
onChange={(e) => setEditLocation(e.target.value)}
|
||||||
placeholder="Where do you do this? e.g., bathroom, kitchen"
|
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>
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={editHabitStack}
|
value={editHabitStack}
|
||||||
onChange={(e) => setEditHabitStack(e.target.value)}
|
onChange={(e) => setEditHabitStack(e.target.value)}
|
||||||
placeholder="What do you do right before? e.g., finish breakfast"
|
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>
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium text-gray-700 mb-1">Environment prompts</label>
|
<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 mb-2">Quick checklist shown before starting</p>
|
<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">
|
<div className="space-y-2 mb-2">
|
||||||
{editEnvPrompts.map((prompt, i) => (
|
{editEnvPrompts.map((prompt, i) => (
|
||||||
<div key={i} className="flex items-center gap-2">
|
<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
|
<button
|
||||||
onClick={() => setEditEnvPrompts(editEnvPrompts.filter((_, j) => j !== i))}
|
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
|
Remove
|
||||||
</button>
|
</button>
|
||||||
@@ -366,7 +366,7 @@ export default function RoutineDetailPage() {
|
|||||||
value={newEnvPrompt}
|
value={newEnvPrompt}
|
||||||
onChange={(e) => setNewEnvPrompt(e.target.value)}
|
onChange={(e) => setNewEnvPrompt(e.target.value)}
|
||||||
placeholder="e.g., Water bottle nearby?"
|
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) => {
|
onKeyDown={(e) => {
|
||||||
if (e.key === 'Enter' && newEnvPrompt.trim()) {
|
if (e.key === 'Enter' && newEnvPrompt.trim()) {
|
||||||
setEditEnvPrompts([...editEnvPrompts, newEnvPrompt.trim()]);
|
setEditEnvPrompts([...editEnvPrompts, newEnvPrompt.trim()]);
|
||||||
@@ -381,7 +381,7 @@ export default function RoutineDetailPage() {
|
|||||||
setNewEnvPrompt('');
|
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
|
Add
|
||||||
</button>
|
</button>
|
||||||
@@ -401,7 +401,7 @@ export default function RoutineDetailPage() {
|
|||||||
setEditHabitStack(routine.habit_stack_after || '');
|
setEditHabitStack(routine.habit_stack_after || '');
|
||||||
setEditEnvPrompts(routine.environment_prompts || []);
|
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
|
Cancel
|
||||||
</button>
|
</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="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>
|
<span className="text-4xl">{routine.icon || '✨'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<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 && (
|
{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">
|
<span className="flex items-center gap-1">
|
||||||
<ClockIcon size={14} />
|
<ClockIcon size={14} />
|
||||||
{totalDuration} min
|
{totalDuration} min
|
||||||
@@ -444,16 +444,16 @@ export default function RoutineDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Schedule display (view mode) */}
|
{/* 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 justify-between mb-2">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<ClockIcon size={16} className="text-indigo-500" />
|
<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>
|
</div>
|
||||||
{!showScheduleEditor && (
|
{!showScheduleEditor && (
|
||||||
<button
|
<button
|
||||||
onClick={() => setShowScheduleEditor(true)}
|
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'}
|
{schedule ? 'Edit' : 'Add schedule'}
|
||||||
</button>
|
</button>
|
||||||
@@ -468,7 +468,7 @@ export default function RoutineDetailPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
|
onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
|
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
|
Every day
|
||||||
@@ -477,7 +477,7 @@ export default function RoutineDetailPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
|
onClick={() => setEditDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
|
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
|
Weekdays
|
||||||
@@ -486,7 +486,7 @@ export default function RoutineDetailPage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setEditDays(['sat', 'sun'])}
|
onClick={() => setEditDays(['sat', 'sun'])}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
|
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
|
Weekends
|
||||||
@@ -494,7 +494,7 @@ export default function RoutineDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mb-3">
|
<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">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{DAY_OPTIONS.map((day) => (
|
{DAY_OPTIONS.map((day) => (
|
||||||
<button
|
<button
|
||||||
@@ -504,7 +504,7 @@ export default function RoutineDetailPage() {
|
|||||||
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
||||||
editDays.includes(day.value)
|
editDays.includes(day.value)
|
||||||
? 'bg-indigo-600 text-white border-indigo-600'
|
? '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}
|
{day.label}
|
||||||
@@ -513,23 +513,23 @@ export default function RoutineDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-3">
|
<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
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={editTime}
|
value={editTime}
|
||||||
onChange={(e) => setEditTime(e.target.value)}
|
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>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<div className="flex items-center justify-between mb-3">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Send reminder</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Send reminder</p>
|
||||||
<p className="text-sm text-gray-500">Get notified when it's time</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Get notified when it's time</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => setEditRemind(!editRemind)}
|
onClick={() => setEditRemind(!editRemind)}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<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);
|
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
|
Cancel
|
||||||
</button>
|
</button>
|
||||||
@@ -565,15 +565,15 @@ export default function RoutineDetailPage() {
|
|||||||
</>
|
</>
|
||||||
) : schedule && schedule.days.length > 0 ? (
|
) : 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}
|
{formatDays(schedule.days)} at {schedule.time}
|
||||||
</p>
|
</p>
|
||||||
{schedule.remind && (
|
{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>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -582,30 +582,30 @@ export default function RoutineDetailPage() {
|
|||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<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 && (
|
{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>
|
</div>
|
||||||
{steps.length > 7 && (
|
{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.
|
Tip: Routines with 4-7 steps tend to feel more manageable. Consider combining related steps.
|
||||||
</p>
|
</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">
|
<div className="flex gap-2">
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={newStepName}
|
value={newStepName}
|
||||||
onChange={(e) => setNewStepName(e.target.value)}
|
onChange={(e) => setNewStepName(e.target.value)}
|
||||||
placeholder="New step name"
|
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
|
<select
|
||||||
value={newStepDuration}
|
value={newStepDuration}
|
||||||
onChange={(e) => setNewStepDuration(Number(e.target.value))}
|
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={1}>1m</option>
|
||||||
<option value={5}>5m</option>
|
<option value={5}>5m</option>
|
||||||
@@ -623,44 +623,44 @@ export default function RoutineDetailPage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{steps.length === 0 ? (
|
{steps.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
|
||||||
<p className="text-gray-500">No steps yet</p>
|
<p className="text-gray-500 dark:text-gray-400">No steps yet</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{steps.map((step, index) => (
|
{steps.map((step, index) => (
|
||||||
<div
|
<div
|
||||||
key={step.id}
|
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}
|
{index + 1}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<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 && (
|
{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>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMoveStep(step.id, 'up')}
|
onClick={() => handleMoveStep(step.id, 'up')}
|
||||||
disabled={index === 0}
|
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>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleMoveStep(step.id, 'down')}
|
onClick={() => handleMoveStep(step.id, 'down')}
|
||||||
disabled={index === steps.length - 1}
|
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>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleDeleteStep(step.id)}
|
onClick={() => handleDeleteStep(step.id)}
|
||||||
className="text-red-500 p-2"
|
className="text-red-500 dark:text-red-400 p-2"
|
||||||
>
|
>
|
||||||
<TrashIcon size={18} />
|
<TrashIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -116,26 +116,26 @@ export default function NewRoutinePage() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-950">
|
||||||
<header className="bg-white border-b border-gray-200 sticky top-0 z-10">
|
<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">
|
<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} />
|
<ArrowLeftIcon size={24} />
|
||||||
</button>
|
</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>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/templates"
|
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">
|
<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" />
|
<CopyIcon size={24} className="text-indigo-600 dark:text-indigo-400" />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-semibold text-gray-900">Start from a template</p>
|
<p className="font-semibold text-gray-900 dark:text-gray-100">Start from a template</p>
|
||||||
<p className="text-sm text-gray-500">Browse pre-made routines</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Browse pre-made routines</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-indigo-600 text-white text-xs font-medium px-2 py-1 rounded-full">
|
<div className="bg-indigo-600 text-white text-xs font-medium px-2 py-1 rounded-full">
|
||||||
Recommended
|
Recommended
|
||||||
@@ -144,15 +144,15 @@ export default function NewRoutinePage() {
|
|||||||
|
|
||||||
<form onSubmit={handleSubmit} className="p-4 space-y-6">
|
<form onSubmit={handleSubmit} className="p-4 space-y-6">
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Basic Info */}
|
{/* 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>
|
<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">
|
<div className="flex flex-wrap gap-2">
|
||||||
{ICONS.map((i) => (
|
{ICONS.map((i) => (
|
||||||
<button
|
<button
|
||||||
@@ -161,8 +161,8 @@ export default function NewRoutinePage() {
|
|||||||
onClick={() => setIcon(i)}
|
onClick={() => setIcon(i)}
|
||||||
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition ${
|
className={`w-10 h-10 rounded-lg text-xl flex items-center justify-center transition ${
|
||||||
icon === i
|
icon === i
|
||||||
? 'bg-indigo-100 ring-2 ring-indigo-600'
|
? 'bg-indigo-100 dark:bg-indigo-900/50 ring-2 ring-indigo-600'
|
||||||
: 'bg-gray-100 hover:bg-gray-200'
|
: 'bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{i}
|
{i}
|
||||||
@@ -172,31 +172,31 @@ export default function NewRoutinePage() {
|
|||||||
</div>
|
</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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={name}
|
value={name}
|
||||||
onChange={(e) => setName(e.target.value)}
|
onChange={(e) => setName(e.target.value)}
|
||||||
placeholder="Morning Routine"
|
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>
|
||||||
|
|
||||||
<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
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={(e) => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
placeholder="Start your day right"
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Schedule */}
|
{/* Schedule */}
|
||||||
<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">
|
||||||
<h2 className="text-lg font-semibold text-gray-900">Schedule <span className="text-sm font-normal text-gray-400">(optional)</span></h2>
|
<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 */}
|
{/* Quick select buttons */}
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
@@ -204,7 +204,7 @@ export default function NewRoutinePage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setScheduleDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
|
onClick={() => setScheduleDays(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun'])}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
|
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
|
Every day
|
||||||
@@ -213,7 +213,7 @@ export default function NewRoutinePage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setScheduleDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
|
onClick={() => setScheduleDays(['mon', 'tue', 'wed', 'thu', 'fri'])}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
|
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
|
Weekdays
|
||||||
@@ -222,7 +222,7 @@ export default function NewRoutinePage() {
|
|||||||
type="button"
|
type="button"
|
||||||
onClick={() => setScheduleDays(['sat', 'sun'])}
|
onClick={() => setScheduleDays(['sat', 'sun'])}
|
||||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium border transition-colors ${
|
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
|
Weekends
|
||||||
@@ -230,7 +230,7 @@ export default function NewRoutinePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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">
|
<div className="flex gap-2 flex-wrap">
|
||||||
{DAY_OPTIONS.map((day) => (
|
{DAY_OPTIONS.map((day) => (
|
||||||
<button
|
<button
|
||||||
@@ -240,7 +240,7 @@ export default function NewRoutinePage() {
|
|||||||
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
className={`px-3 py-2 rounded-lg text-sm font-medium border transition-colors ${
|
||||||
scheduleDays.includes(day.value)
|
scheduleDays.includes(day.value)
|
||||||
? 'bg-indigo-600 text-white border-indigo-600'
|
? '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}
|
{day.label}
|
||||||
@@ -250,25 +250,25 @@ export default function NewRoutinePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
<input
|
||||||
type="time"
|
type="time"
|
||||||
value={scheduleTime}
|
value={scheduleTime}
|
||||||
onChange={(e) => setScheduleTime(e.target.value)}
|
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>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Send reminder</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Send reminder</p>
|
||||||
<p className="text-sm text-gray-500">Get notified when it's time</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Get notified when it's time</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setScheduleRemind(!scheduleRemind)}
|
onClick={() => setScheduleRemind(!scheduleRemind)}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
@@ -281,11 +281,11 @@ export default function NewRoutinePage() {
|
|||||||
{/* Steps */}
|
{/* Steps */}
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center justify-between mb-3">
|
<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
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddStep}
|
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} />
|
<PlusIcon size={16} />
|
||||||
Add Step
|
Add Step
|
||||||
@@ -293,12 +293,12 @@ export default function NewRoutinePage() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{steps.length === 0 ? (
|
{steps.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-xl p-8 shadow-sm text-center">
|
||||||
<p className="text-gray-500 mb-4">Add steps to your routine</p>
|
<p className="text-gray-500 dark:text-gray-400 mb-4">Add steps to your routine</p>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={handleAddStep}
|
onClick={handleAddStep}
|
||||||
className="text-indigo-600 font-medium"
|
className="text-indigo-600 dark:text-indigo-400 font-medium"
|
||||||
>
|
>
|
||||||
+ Add your first step
|
+ Add your first step
|
||||||
</button>
|
</button>
|
||||||
@@ -308,9 +308,9 @@ export default function NewRoutinePage() {
|
|||||||
{steps.map((step, index) => (
|
{steps.map((step, index) => (
|
||||||
<div
|
<div
|
||||||
key={step.id}
|
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} />
|
<GripVerticalIcon size={20} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 space-y-3">
|
<div className="flex-1 space-y-3">
|
||||||
@@ -319,14 +319,14 @@ export default function NewRoutinePage() {
|
|||||||
value={step.name}
|
value={step.name}
|
||||||
onChange={(e) => handleUpdateStep(index, { name: e.target.value })}
|
onChange={(e) => handleUpdateStep(index, { name: e.target.value })}
|
||||||
placeholder="Step name"
|
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">
|
<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
|
<select
|
||||||
value={step.duration_minutes || 5}
|
value={step.duration_minutes || 5}
|
||||||
onChange={(e) => handleUpdateStep(index, { duration_minutes: Number(e.target.value) })}
|
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={1}>1 min</option>
|
||||||
<option value={2}>2 min</option>
|
<option value={2}>2 min</option>
|
||||||
@@ -343,7 +343,7 @@ export default function NewRoutinePage() {
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => handleDeleteStep(index)}
|
onClick={() => handleDeleteStep(index)}
|
||||||
className="text-red-500 p-2"
|
className="text-red-500 dark:text-red-400 p-2"
|
||||||
>
|
>
|
||||||
<TrashIcon size={18} />
|
<TrashIcon size={18} />
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -475,10 +475,10 @@ export default function RoutinesPage() {
|
|||||||
return (
|
return (
|
||||||
// h-screen + overflow-hidden eliminates the outer page scrollbar;
|
// h-screen + overflow-hidden eliminates the outer page scrollbar;
|
||||||
// only the inner timeline div scrolls.
|
// 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 */}
|
{/* Header */}
|
||||||
<div className="flex items-center justify-between px-4 pt-4 pb-2 bg-white border-b border-gray-100 flex-shrink-0">
|
<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">Routines</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Routines</h1>
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/routines/new"
|
href="/dashboard/routines/new"
|
||||||
className="bg-indigo-600 text-white p-2 rounded-full"
|
className="bg-indigo-600 text-white p-2 rounded-full"
|
||||||
@@ -516,7 +516,7 @@ export default function RoutinesPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Week Strip */}
|
{/* 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) => {
|
{weekDays.map((day, i) => {
|
||||||
const selected = isSameDay(day, selectedDate);
|
const selected = isSameDay(day, selectedDate);
|
||||||
const isTodayDay = isSameDay(day, today);
|
const isTodayDay = isSameDay(day, today);
|
||||||
@@ -526,7 +526,7 @@ export default function RoutinesPage() {
|
|||||||
onClick={() => setSelectedDate(day)}
|
onClick={() => setSelectedDate(day)}
|
||||||
className="flex-1 flex flex-col items-center py-1 rounded-xl"
|
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]}
|
{DAY_LABELS[i]}
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
@@ -536,8 +536,8 @@ export default function RoutinesPage() {
|
|||||||
: isTodayDay
|
: isTodayDay
|
||||||
? 'text-indigo-600 font-bold'
|
? 'text-indigo-600 font-bold'
|
||||||
: selected
|
: selected
|
||||||
? 'bg-gray-200 text-gray-900'
|
? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100'
|
||||||
: 'text-gray-700'
|
: 'text-gray-700 dark:text-gray-300'
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{day.getDate()}
|
{day.getDate()}
|
||||||
@@ -556,10 +556,10 @@ export default function RoutinesPage() {
|
|||||||
|
|
||||||
{allRoutines.length === 0 ? (
|
{allRoutines.length === 0 ? (
|
||||||
<div className="flex flex-col items-center justify-center py-20 px-4">
|
<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" />
|
<ClockIcon size={32} className="text-indigo-400" />
|
||||||
</div>
|
</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
|
<Link
|
||||||
href="/dashboard/routines/new"
|
href="/dashboard/routines/new"
|
||||||
className="bg-indigo-600 text-white px-6 py-3 rounded-xl font-medium"
|
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`,
|
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}
|
{label}
|
||||||
</span>
|
</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>
|
</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 ${
|
className={`absolute rounded-xl px-3 py-2 text-left shadow-sm border transition-all overflow-hidden ${
|
||||||
isPast
|
isPast
|
||||||
? 'bg-green-50 border-green-200 opacity-75'
|
? 'bg-green-50 dark:bg-green-900/30 border-green-200 dark:border-green-800 opacity-75'
|
||||||
: 'bg-indigo-50 border-indigo-200'
|
: 'bg-indigo-50 dark:bg-indigo-900/30 border-indigo-200 dark:border-indigo-800'
|
||||||
} ${isCurrent ? 'ring-2 ring-indigo-500 opacity-100' : ''}`}
|
} ${isCurrent ? 'ring-2 ring-indigo-500 opacity-100' : ''}`}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -651,10 +651,10 @@ export default function RoutinesPage() {
|
|||||||
{entry.routine_icon || '✨'}
|
{entry.routine_icon || '✨'}
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<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}
|
{entry.routine_name}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 truncate">
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
{formatTime(entry.time)}
|
{formatTime(entry.time)}
|
||||||
{entry.total_duration_minutes > 0 && (
|
{entry.total_duration_minutes > 0 && (
|
||||||
<>
|
<>
|
||||||
@@ -687,13 +687,13 @@ export default function RoutinesPage() {
|
|||||||
totalLanes: 1,
|
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)
|
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)
|
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)
|
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 (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -711,10 +711,10 @@ export default function RoutinesPage() {
|
|||||||
💊
|
💊
|
||||||
</span>
|
</span>
|
||||||
<div className="min-w-0 flex-1">
|
<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)}
|
{formatMedsList(group.medications)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-gray-500 truncate">
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
{formatTime(group.time)}
|
{formatTime(group.time)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
Skip
|
||||||
</button>
|
</button>
|
||||||
@@ -793,23 +793,23 @@ export default function RoutinesPage() {
|
|||||||
|
|
||||||
{/* Unscheduled routines — inside the scroll area, below the timeline */}
|
{/* Unscheduled routines — inside the scroll area, below the timeline */}
|
||||||
{unscheduledRoutines.length > 0 && (
|
{unscheduledRoutines.length > 0 && (
|
||||||
<div className="border-t border-gray-200 bg-white px-4 pt-3 pb-4">
|
<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 uppercase tracking-wide mb-2">
|
<h2 className="text-xs font-semibold text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">
|
||||||
Unscheduled
|
Unscheduled
|
||||||
</h2>
|
</h2>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{unscheduledRoutines.map((r) => (
|
{unscheduledRoutines.map((r) => (
|
||||||
<div
|
<div
|
||||||
key={r.id}
|
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>
|
<span className="text-xl flex-shrink-0">{r.icon || '✨'}</span>
|
||||||
<div className="flex-1 min-w-0">
|
<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}
|
{r.name}
|
||||||
</p>
|
</p>
|
||||||
{r.description && (
|
{r.description && (
|
||||||
<p className="text-xs text-gray-500 truncate">
|
<p className="text-xs text-gray-500 dark:text-gray-400 truncate">
|
||||||
{r.description}
|
{r.description}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -90,27 +90,27 @@ export default function SettingsPage() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-6">
|
<div className="p-4 space-y-6">
|
||||||
<div className="flex items-center justify-between">
|
<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 && (
|
{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>
|
</div>
|
||||||
|
|
||||||
{/* Session Experience */}
|
{/* Session Experience */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Session Experience</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Session Experience</h2>
|
||||||
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
{/* Sound */}
|
{/* Sound */}
|
||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
{prefs.sound_enabled ? (
|
{prefs.sound_enabled ? (
|
||||||
<VolumeIcon size={20} className="text-indigo-500" />
|
<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>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Sound effects</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Sound effects</p>
|
||||||
<p className="text-sm text-gray-500">Subtle audio cues on step completion</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Subtle audio cues on step completion</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -119,7 +119,7 @@ export default function SettingsPage() {
|
|||||||
if (!prefs.sound_enabled) playStepComplete();
|
if (!prefs.sound_enabled) playStepComplete();
|
||||||
}}
|
}}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
@@ -131,10 +131,10 @@ export default function SettingsPage() {
|
|||||||
{/* Haptics */}
|
{/* Haptics */}
|
||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div className="flex items-center gap-3">
|
<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>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Haptic feedback</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Haptic feedback</p>
|
||||||
<p className="text-sm text-gray-500">Gentle vibration on actions</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Gentle vibration on actions</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
@@ -143,7 +143,7 @@ export default function SettingsPage() {
|
|||||||
if (!prefs.haptic_enabled) hapticTap();
|
if (!prefs.haptic_enabled) hapticTap();
|
||||||
}}
|
}}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<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 */}
|
{/* Launch Screen */}
|
||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Pre-routine launch screen</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Pre-routine launch screen</p>
|
||||||
<p className="text-sm text-gray-500">Environment check and emotion bridge</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Environment check and emotion bridge</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => updatePref('show_launch_screen', !prefs.show_launch_screen)}
|
onClick={() => updatePref('show_launch_screen', !prefs.show_launch_screen)}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
@@ -174,8 +174,8 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Notifications */}
|
{/* Notifications */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Notifications</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Notifications</h2>
|
||||||
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
|
<div className="bg-white dark:bg-gray-800 rounded-xl shadow-sm divide-y divide-gray-100 dark:divide-gray-700">
|
||||||
{/* Push Notifications */}
|
{/* Push Notifications */}
|
||||||
<PushNotificationToggle />
|
<PushNotificationToggle />
|
||||||
|
|
||||||
@@ -183,13 +183,13 @@ export default function SettingsPage() {
|
|||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">ntfy</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">ntfy</p>
|
||||||
<p className="text-sm text-gray-500">Push notifications via ntfy.sh</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Push notifications via ntfy.sh</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateNotif({ ntfy_enabled: !notif.ntfy_enabled })}
|
onClick={() => updateNotif({ ntfy_enabled: !notif.ntfy_enabled })}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<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}
|
value={notif.ntfy_topic}
|
||||||
onChange={(e) => setNotif({ ...notif, ntfy_topic: e.target.value })}
|
onChange={(e) => setNotif({ ...notif, ntfy_topic: e.target.value })}
|
||||||
onBlur={() => updateNotif({ ntfy_topic: notif.ntfy_topic })}
|
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>
|
</div>
|
||||||
@@ -213,13 +213,13 @@ export default function SettingsPage() {
|
|||||||
<div className="p-4 space-y-3">
|
<div className="p-4 space-y-3">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Discord</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Discord</p>
|
||||||
<p className="text-sm text-gray-500">Get DMs from the Synculous bot</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Get DMs from the Synculous bot</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => updateNotif({ discord_enabled: !notif.discord_enabled })}
|
onClick={() => updateNotif({ discord_enabled: !notif.discord_enabled })}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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 ${
|
<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}
|
value={notif.discord_user_id}
|
||||||
onChange={(e) => setNotif({ ...notif, discord_user_id: e.target.value })}
|
onChange={(e) => setNotif({ ...notif, discord_user_id: e.target.value })}
|
||||||
onBlur={() => updateNotif({ discord_user_id: notif.discord_user_id })}
|
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>
|
</div>
|
||||||
@@ -243,8 +243,8 @@ export default function SettingsPage() {
|
|||||||
|
|
||||||
{/* Celebration Style */}
|
{/* Celebration Style */}
|
||||||
<div>
|
<div>
|
||||||
<h2 className="text-lg font-semibold text-gray-900 mb-3">Celebration Style</h2>
|
<h2 className="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-3">Celebration Style</h2>
|
||||||
<div className="bg-white rounded-xl shadow-sm divide-y divide-gray-100">
|
<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: 'standard', label: 'Standard', desc: 'Full animated celebration with stats and rewards' },
|
||||||
{ value: 'quick', label: 'Quick', desc: 'Brief confirmation, then back to dashboard' },
|
{ 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"
|
className="w-full flex items-center justify-between p-4 text-left"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">{option.label}</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">{option.label}</p>
|
||||||
<p className="text-sm text-gray-500">{option.desc}</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">{option.desc}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
<div className={`w-5 h-5 rounded-full border-2 flex items-center justify-center ${
|
||||||
prefs.celebration_style === option.value
|
prefs.celebration_style === option.value
|
||||||
? 'border-indigo-500'
|
? 'border-indigo-500'
|
||||||
: 'border-gray-300'
|
: 'border-gray-300 dark:border-gray-600'
|
||||||
}`}>
|
}`}>
|
||||||
{prefs.celebration_style === option.value && (
|
{prefs.celebration_style === option.value && (
|
||||||
<div className="w-2.5 h-2.5 rounded-full bg-indigo-500" />
|
<div className="w-2.5 h-2.5 rounded-full bg-indigo-500" />
|
||||||
|
|||||||
@@ -122,7 +122,7 @@ export default function StatsPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-6">
|
<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 */}
|
{/* Weekly Summary */}
|
||||||
{weeklySummary && (
|
{weeklySummary && (
|
||||||
@@ -148,11 +148,11 @@ export default function StatsPage() {
|
|||||||
{/* Wins This Month */}
|
{/* Wins This Month */}
|
||||||
{victories.length > 0 && (
|
{victories.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="space-y-2">
|
||||||
{victories.map((victory, i) => (
|
{victories.map((victory, i) => (
|
||||||
<div key={i} className="bg-white rounded-xl p-4 shadow-sm flex items-center gap-3">
|
<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 rounded-xl flex items-center justify-center">
|
<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">
|
<span className="text-lg">
|
||||||
{victory.type === 'comeback' ? '💪' :
|
{victory.type === 'comeback' ? '💪' :
|
||||||
victory.type === 'weekend' ? '🎉' :
|
victory.type === 'weekend' ? '🎉' :
|
||||||
@@ -160,7 +160,7 @@ export default function StatsPage() {
|
|||||||
victory.type === 'consistency' ? '🔥' : '⭐'}
|
victory.type === 'consistency' ? '🔥' : '⭐'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
@@ -170,16 +170,16 @@ export default function StatsPage() {
|
|||||||
{/* Consistency (formerly Streaks) */}
|
{/* Consistency (formerly Streaks) */}
|
||||||
{streaks.length > 0 && (
|
{streaks.length > 0 && (
|
||||||
<div>
|
<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">
|
<div className="space-y-2">
|
||||||
{streaks.map((streak) => (
|
{streaks.map((streak) => (
|
||||||
<div key={streak.routine_id} className="bg-white rounded-xl p-4 shadow-sm flex items-center gap-4">
|
<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 rounded-xl flex items-center justify-center">
|
<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} />
|
<FlameIcon className="text-orange-500" size={24} />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1">
|
<div className="flex-1">
|
||||||
<p className="font-medium text-gray-900">{streak.routine_name}</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">{streak.routine_name}</p>
|
||||||
<p className="text-sm text-gray-500">
|
<p className="text-sm text-gray-500 dark:text-gray-400">
|
||||||
{getStreakMessage(streak.current_streak)}
|
{getStreakMessage(streak.current_streak)}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,17 +187,17 @@ export default function StatsPage() {
|
|||||||
{streak.current_streak > 0 ? (
|
{streak.current_streak > 0 ? (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-bold text-orange-500">{streak.current_streak}</p>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{/* Longest streak callout */}
|
{/* Longest streak callout */}
|
||||||
{streaks.some(s => s.longest_streak > 0) && (
|
{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've done it before
|
Your personal best: {Math.max(...streaks.map(s => s.longest_streak))} days — you've done it before
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -208,11 +208,11 @@ export default function StatsPage() {
|
|||||||
{/* Per-Routine Stats */}
|
{/* Per-Routine Stats */}
|
||||||
{routines.length > 0 && (
|
{routines.length > 0 && (
|
||||||
<div>
|
<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
|
<select
|
||||||
value={selectedRoutine}
|
value={selectedRoutine}
|
||||||
onChange={(e) => setSelectedRoutine(e.target.value)}
|
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) => (
|
{routines.map((routine) => (
|
||||||
<option key={routine.id} value={routine.id}>
|
<option key={routine.id} value={routine.id}>
|
||||||
@@ -223,34 +223,34 @@ export default function StatsPage() {
|
|||||||
|
|
||||||
{routineStats && (
|
{routineStats && (
|
||||||
<div className="grid grid-cols-2 gap-3">
|
<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} />
|
<TargetIcon className="text-indigo-500 mb-2" size={24} />
|
||||||
<p className={`text-lg font-bold ${getCompletionLabel(routineStats.completion_rate_percent).color}`}>
|
<p className={`text-lg font-bold ${getCompletionLabel(routineStats.completion_rate_percent).color}`}>
|
||||||
{getCompletionLabel(routineStats.completion_rate_percent).label}
|
{getCompletionLabel(routineStats.completion_rate_percent).label}
|
||||||
</p>
|
</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>
|
||||||
<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} />
|
<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-2xl font-bold text-gray-900 dark:text-gray-100">{formatTime(routineStats.avg_duration_minutes)}</p>
|
||||||
<p className="text-sm text-gray-500">Avg Duration</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Avg Duration</p>
|
||||||
</div>
|
</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} />
|
<StarIcon className="text-green-500 mb-2" size={24} />
|
||||||
<p className="text-2xl font-bold text-gray-900">{routineStats.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">Completed</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Completed</p>
|
||||||
</div>
|
</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} />
|
<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-2xl font-bold text-gray-900 dark:text-gray-100">{routineStats.total_sessions}</p>
|
||||||
<p className="text-sm text-gray-500">Total Sessions</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Total Sessions</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Plateau messaging */}
|
{/* Plateau messaging */}
|
||||||
{routineStats && routineStats.total_sessions >= 5 && (
|
{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
|
{routineStats.completion_rate_percent >= 60
|
||||||
? "You're showing up consistently — that's the hard part. The exact rate doesn't matter."
|
? "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."}
|
: "Life has seasons. The fact that you're checking in shows this matters to you."}
|
||||||
|
|||||||
@@ -54,34 +54,34 @@ export default function TemplatesPage() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4 space-y-4">
|
<div className="p-4 space-y-4">
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Templates</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Templates</h1>
|
||||||
<p className="text-gray-500">Start with a pre-made routine</p>
|
<p className="text-gray-500 dark:text-gray-400">Start with a pre-made routine</p>
|
||||||
|
|
||||||
{templates.length === 0 ? (
|
{templates.length === 0 ? (
|
||||||
<div className="bg-white rounded-xl p-8 shadow-sm text-center mt-4">
|
<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 rounded-full flex items-center justify-center mx-auto mb-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" size={32} />
|
<FlameIcon className="text-gray-400 dark:text-gray-500" size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h3 className="font-semibold text-gray-900 mb-1">No templates yet</h3>
|
<h3 className="font-semibold text-gray-900 dark:text-gray-100 mb-1">No templates yet</h3>
|
||||||
<p className="text-gray-500 text-sm">Templates will appear here when available</p>
|
<p className="text-gray-500 dark:text-gray-400 text-sm">Templates will appear here when available</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
{templates.map((template) => (
|
{templates.map((template) => (
|
||||||
<div
|
<div
|
||||||
key={template.id}
|
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="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>
|
<span className="text-3xl">{template.icon || '✨'}</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
<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 && (
|
{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>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={() => handleClone(template.id)}
|
onClick={() => handleClone(template.id)}
|
||||||
|
|||||||
@@ -1,10 +1,17 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dark {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
--color-foreground: var(--foreground);
|
--color-foreground: var(--foreground);
|
||||||
@@ -12,13 +19,6 @@
|
|||||||
--font-mono: var(--font-geist-mono);
|
--font-mono: var(--font-geist-mono);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
|
||||||
:root {
|
|
||||||
--background: #0a0a0a;
|
|
||||||
--foreground: #ededed;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
color: var(--foreground);
|
color: var(--foreground);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
import "./globals.css";
|
import "./globals.css";
|
||||||
import { AuthProvider } from "@/components/auth/AuthProvider";
|
import { AuthProvider } from "@/components/auth/AuthProvider";
|
||||||
|
import { ThemeProvider } from "@/components/theme/ThemeProvider";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Synculous",
|
title: "Synculous",
|
||||||
@@ -31,9 +32,16 @@ export default function RootLayout({
|
|||||||
<link rel="apple-touch-icon" href="/icon-192.png" />
|
<link rel="apple-touch-icon" href="/icon-192.png" />
|
||||||
</head>
|
</head>
|
||||||
<body className="antialiased">
|
<body className="antialiased">
|
||||||
|
<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>
|
<AuthProvider>
|
||||||
{children}
|
{children}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
|
</ThemeProvider>
|
||||||
<script
|
<script
|
||||||
dangerouslySetInnerHTML={{
|
dangerouslySetInnerHTML={{
|
||||||
__html: `if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js')}`,
|
__html: `if('serviceWorker' in navigator){navigator.serviceWorker.register('/sw.js')}`,
|
||||||
|
|||||||
@@ -36,47 +36,47 @@ export default function LoginPage() {
|
|||||||
|
|
||||||
return (
|
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="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="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">
|
<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} />
|
<HeartIcon className="text-white" size={32} />
|
||||||
</div>
|
</div>
|
||||||
<h1 className="text-2xl font-bold text-gray-900">Synculous</h1>
|
<h1 className="text-2xl font-bold text-gray-900 dark:text-gray-100">Synculous</h1>
|
||||||
<p className="text-gray-500 mt-1">
|
<p className="text-gray-500 dark:text-gray-400 mt-1">
|
||||||
{isLogin ? 'Welcome back!' : 'Create your account'}
|
{isLogin ? 'Welcome back!' : 'Create your account'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
{error && (
|
{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}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<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
|
Username
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
value={username}
|
value={username}
|
||||||
onChange={(e) => setUsername(e.target.value)}
|
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"
|
placeholder="Enter your username"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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
|
Password
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
value={password}
|
value={password}
|
||||||
onChange={(e) => setPassword(e.target.value)}
|
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"
|
placeholder="Enter your password"
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ export default function PushNotificationToggle() {
|
|||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-between p-4">
|
<div className="flex items-center justify-between p-4">
|
||||||
<div>
|
<div>
|
||||||
<p className="font-medium text-gray-900">Push notifications</p>
|
<p className="font-medium text-gray-900 dark:text-gray-100">Push notifications</p>
|
||||||
<p className="text-sm text-gray-500">Get reminders on this device</p>
|
<p className="text-sm text-gray-500 dark:text-gray-400">Get reminders on this device</p>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
onClick={toggle}
|
onClick={toggle}
|
||||||
disabled={loading}
|
disabled={loading}
|
||||||
className={`w-12 h-7 rounded-full transition-colors ${
|
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' : ''}`}
|
} ${loading ? 'opacity-50' : ''}`}
|
||||||
>
|
>
|
||||||
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
<div className={`w-5 h-5 bg-white rounded-full shadow transition-transform ml-1 ${
|
||||||
|
|||||||
52
synculous-client/src/components/theme/ThemeProvider.tsx
Normal file
52
synculous-client/src/components/theme/ThemeProvider.tsx
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user