diff --git a/web/public/manifest.json b/web/public/manifest.json new file mode 100644 index 0000000..4c0ba73 --- /dev/null +++ b/web/public/manifest.json @@ -0,0 +1,29 @@ +{ + "name": "ChronoMind — Smart Pre-Warning Timer", + "short_name": "ChronoMind", + "description": "AI-powered time awareness layer with pre-warning cascades, visual timeline, and Pomodoro sessions.", + "start_url": "/", + "display": "standalone", + "background_color": "#06070A", + "theme_color": "#5A8CFF", + "orientation": "portrait-primary", + "icons": [ + { + "src": "/icons/icon-192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/icons/icon-512.png", + "sizes": "512x512", + "type": "image/png" + }, + { + "src": "/icons/icon-512-maskable.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "maskable" + } + ], + "categories": ["productivity", "utilities"] +} diff --git a/web/src/app/landing/page.tsx b/web/src/app/landing/page.tsx new file mode 100644 index 0000000..75132d9 --- /dev/null +++ b/web/src/app/landing/page.tsx @@ -0,0 +1,176 @@ +import Link from 'next/link'; +import { Clock, Bell, Zap, Timer, Coffee, BarChart3, Shield, ArrowRight } from 'lucide-react'; + +const FEATURES = [ + { + icon: , + title: 'Pre-Warning Cascades', + description: 'Get notified 2 hours, 1 hour, 30 minutes, and 5 minutes before your timer fires. Never be caught off-guard.', + }, + { + icon: , + title: 'Visual Timeline', + description: 'See all your timers on a timeline with urgency-coded colors. Instant awareness of what\'s next.', + }, + { + icon: , + title: '5 Urgency Levels', + description: 'From passive badges to full-screen critical alerts. Each timer gets the attention it deserves.', + }, + { + icon: , + title: 'Pomodoro Built-In', + description: 'Focus sessions with configurable work/break/rounds. Beautiful countdown ring with round tracking.', + }, + { + icon: , + title: 'Smart Defaults', + description: 'Neurodivergent-friendly design by default. Large countdown rings, gentle transitions, time blindness aids.', + }, + { + icon: , + title: 'Privacy-First', + description: 'All data stored locally in your browser. No accounts. No tracking. No servers. Yours alone.', + }, +]; + +export default function LandingPage() { + return ( +
+ {/* Nav */} + + + {/* Hero */} +
+
+ Free · No signup · Works offline +
+ +

+ Never be caught +
+ off-guard again +

+ +

+ ChronoMind warns you before timers fire — with escalating pre-warnings at 2 hours, + 1 hour, 30 minutes, and 5 minutes. The timer you always wished your phone had. +

+ +
+ + Try it now — free + + + GitHub + +
+
+ + {/* Feature grid */} +
+

+ Why ChronoMind? +

+
+ {FEATURES.map((feature) => ( +
+
+ {feature.icon} +
+

+ {feature.title} +

+

+ {feature.description} +

+
+ ))} +
+
+ + {/* Social proof placeholder */} +
+

+ Built for people who struggle with time awareness. Especially helpful for ADHD, time blindness, + and anyone who's ever missed a meeting because "it was only 5 minutes away." +

+
+ + {/* CTA */} +
+

+ Ready to take control of your time? +

+

+ No signup. No credit card. Just better timers. +

+ + Launch ChronoMind + +
+ + {/* Footer */} +
+ © 2026 ChronoMind +
+ Privacy + Terms + GitHub +
+
+
+ ); +} diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index d67009b..ff29bca 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -15,6 +15,16 @@ const jetbrainsMono = JetBrains_Mono({ export const metadata: Metadata = { title: "ChronoMind — Smart Pre-Warning Timer", description: "AI-powered time awareness layer with pre-warning cascades, visual timeline, and Pomodoro sessions.", + manifest: "/manifest.json", + themeColor: "#5A8CFF", + appleWebApp: { + capable: true, + title: "ChronoMind", + statusBarStyle: "black-translucent", + }, + other: { + "mobile-web-app-capable": "yes", + }, }; export default function RootLayout({ diff --git a/web/src/app/privacy/page.tsx b/web/src/app/privacy/page.tsx new file mode 100644 index 0000000..29a52e1 --- /dev/null +++ b/web/src/app/privacy/page.tsx @@ -0,0 +1,100 @@ +import Link from 'next/link'; + +export default function PrivacyPage() { + return ( +
+
+ + ← Back to ChronoMind + + +

+ Privacy Policy +

+

+ Last updated: February 2026 +

+ +
+
+

+ 1. What We Collect +

+

+ Nothing. ChronoMind stores all your data locally in your browser using IndexedDB. + No data is sent to any server. No accounts are required. +

+
+ +
+

+ 2. Local Storage +

+

+ Your timers, routines, categories, and preferences are stored locally in your browser's IndexedDB. + This data never leaves your device. Clearing your browser data will delete all ChronoMind data. +

+
+ +
+

+ 3. Analytics +

+

+ We may collect anonymous, aggregated usage analytics (e.g., number of timers created, features used) + to improve the product. No personally identifiable information is collected. You can opt out in Settings. +

+
+ +
+

+ 4. Notifications +

+

+ ChronoMind requests notification permission to send pre-warning alerts. This is entirely optional + and can be revoked in your browser settings at any time. +

+
+ +
+

+ 5. Cloud Sync (Future) +

+

+ When cloud sync is available (planned for a future release), it will be opt-in. + Data will be encrypted in transit (TLS) and at rest. You will be able to delete all cloud data at any time. +

+
+ +
+

+ 6. Third Parties +

+

+ ChronoMind does not share any data with third parties. There are no ads, no tracking pixels, + and no data brokers. +

+
+ +
+

+ 7. Contact +

+

+ Questions about this policy? Open an issue on our{' '} + + GitHub repository + . +

+
+
+
+
+ ); +} diff --git a/web/src/app/terms/page.tsx b/web/src/app/terms/page.tsx new file mode 100644 index 0000000..be9279a --- /dev/null +++ b/web/src/app/terms/page.tsx @@ -0,0 +1,84 @@ +import Link from 'next/link'; + +export default function TermsPage() { + return ( +
+
+ + ← Back to ChronoMind + + +

+ Terms of Service +

+

+ Last updated: February 2026 +

+ +
+
+

+ 1. Acceptance +

+

+ By using ChronoMind, you agree to these terms. ChronoMind is provided as-is, free of charge + for personal use. +

+
+ +
+

+ 2. Description +

+

+ ChronoMind is a progressive web application (PWA) that provides timer management with + pre-warning cascades, visual timeline, urgency levels, Pomodoro sessions, and + keyboard shortcuts. All data is stored locally in your browser. +

+
+ +
+

+ 3. Data Ownership +

+

+ You own all your data. ChronoMind does not claim any rights to your timer data, + routines, or preferences. Since data is stored locally, you are responsible for backups. +

+
+ +
+

+ 4. Limitations +

+

+ ChronoMind relies on browser APIs for notifications and timing. Notification delivery + and timer accuracy may vary by browser and operating system. ChronoMind is not suitable + for safety-critical timing applications. +

+
+ +
+

+ 5. Disclaimer +

+

+ ChronoMind is provided "as is" without warranty of any kind. We are not liable for + any missed timers, late notifications, or any damages arising from use of the application. +

+
+ +
+

+ 6. Changes +

+

+ We may update these terms as the product evolves. Continued use constitutes acceptance + of updated terms. +

+
+
+
+
+ ); +} diff --git a/web/src/lib/format.test.ts b/web/src/lib/format.test.ts new file mode 100644 index 0000000..669644c --- /dev/null +++ b/web/src/lib/format.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from 'vitest'; +import { formatDuration, formatDurationCompact, formatRelativeTime } from './format'; + +describe('format', () => { + describe('formatDuration', () => { + it('formats zero', () => { + expect(formatDuration(0)).toBe('00:00'); + }); + + it('formats seconds only', () => { + expect(formatDuration(30_000)).toBe('00:30'); + }); + + it('formats minutes and seconds', () => { + expect(formatDuration(5 * 60_000 + 30_000)).toBe('05:30'); + }); + + it('formats hours', () => { + expect(formatDuration(2 * 3600_000 + 15 * 60_000 + 45_000)).toBe('02:15:45'); + }); + + it('handles negative values', () => { + expect(formatDuration(-1000)).toBe('00:00'); + }); + }); + + describe('formatDurationCompact', () => { + it('formats seconds', () => { + expect(formatDurationCompact(45_000)).toBe('45s'); + }); + + it('formats minutes', () => { + expect(formatDurationCompact(15 * 60_000)).toBe('15m'); + }); + + it('formats hours and minutes', () => { + expect(formatDurationCompact(2 * 3600_000 + 30 * 60_000)).toBe('2h 30m'); + }); + + it('formats exact hours', () => { + expect(formatDurationCompact(3 * 3600_000)).toBe('3h'); + }); + + it('handles zero', () => { + expect(formatDurationCompact(0)).toBe('0s'); + }); + }); + + describe('formatRelativeTime', () => { + it('returns "now" for small differences', () => { + const now = Date.now(); + expect(formatRelativeTime(now + 5000, now)).toBe('now'); + }); + + it('returns "in X" for future times', () => { + const now = Date.now(); + const result = formatRelativeTime(now + 5 * 60_000, now); + expect(result).toMatch(/^in /); + }); + + it('returns "X ago" for past times', () => { + const now = Date.now(); + const result = formatRelativeTime(now - 5 * 60_000, now); + expect(result).toMatch(/ ago$/); + }); + }); +});