feat: add PWA manifest, landing page, privacy/terms pages, format tests (53 tests)

This commit is contained in:
saravanakumardb1 2026-02-27 21:00:34 -08:00
parent 6b46384304
commit ace036b1fc
6 changed files with 466 additions and 0 deletions

29
web/public/manifest.json Normal file
View File

@ -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"]
}

View File

@ -0,0 +1,176 @@
import Link from 'next/link';
import { Clock, Bell, Zap, Timer, Coffee, BarChart3, Shield, ArrowRight } from 'lucide-react';
const FEATURES = [
{
icon: <Bell size={28} />,
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: <Timer size={28} />,
title: 'Visual Timeline',
description: 'See all your timers on a timeline with urgency-coded colors. Instant awareness of what\'s next.',
},
{
icon: <Zap size={28} />,
title: '5 Urgency Levels',
description: 'From passive badges to full-screen critical alerts. Each timer gets the attention it deserves.',
},
{
icon: <Coffee size={28} />,
title: 'Pomodoro Built-In',
description: 'Focus sessions with configurable work/break/rounds. Beautiful countdown ring with round tracking.',
},
{
icon: <BarChart3 size={28} />,
title: 'Smart Defaults',
description: 'Neurodivergent-friendly design by default. Large countdown rings, gentle transitions, time blindness aids.',
},
{
icon: <Shield size={28} />,
title: 'Privacy-First',
description: 'All data stored locally in your browser. No accounts. No tracking. No servers. Yours alone.',
},
];
export default function LandingPage() {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
{/* Nav */}
<nav className="max-w-5xl mx-auto px-4 py-4 flex items-center justify-between">
<div className="flex items-center gap-2">
<Clock size={24} style={{ color: 'var(--cm-accent)' }} />
<span className="text-lg font-bold" style={{ color: 'var(--cm-text-primary)' }}>ChronoMind</span>
</div>
<div className="flex items-center gap-4">
<Link href="/privacy" className="text-sm" style={{ color: 'var(--cm-text-tertiary)' }}>Privacy</Link>
<Link
href="/"
className="flex items-center gap-2 px-4 py-2 rounded-xl text-sm font-medium"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
Try it now <ArrowRight size={14} />
</Link>
</div>
</nav>
{/* Hero */}
<section className="max-w-3xl mx-auto px-4 pt-20 pb-16 text-center">
<div
className="inline-flex items-center gap-2 px-4 py-1.5 rounded-full text-xs font-medium mb-6"
style={{
backgroundColor: 'rgba(90, 140, 255, 0.1)',
color: 'var(--cm-accent)',
border: '1px solid rgba(90, 140, 255, 0.2)',
}}
>
<Zap size={12} /> Free &middot; No signup &middot; Works offline
</div>
<h1
className="text-5xl sm:text-6xl font-bold tracking-tight mb-6 leading-tight"
style={{ color: 'var(--cm-text-primary)' }}
>
Never be caught
<br />
<span style={{ color: 'var(--cm-accent)' }}>off-guard</span> again
</h1>
<p className="text-lg max-w-xl mx-auto mb-10" style={{ color: 'var(--cm-text-secondary)' }}>
ChronoMind warns you <em>before</em> timers fire with escalating pre-warnings at 2 hours,
1 hour, 30 minutes, and 5 minutes. The timer you always wished your phone had.
</p>
<div className="flex flex-col sm:flex-row items-center justify-center gap-4">
<Link
href="/"
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-base font-semibold"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
Try it now free <ArrowRight size={16} />
</Link>
<a
href="https://github.com/saravanakumardb1/learning_ai_clock"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-8 py-3.5 rounded-xl text-base font-medium"
style={{
backgroundColor: 'var(--cm-surface-card)',
color: 'var(--cm-text-secondary)',
border: '1px solid var(--cm-border)',
}}
>
GitHub
</a>
</div>
</section>
{/* Feature grid */}
<section className="max-w-5xl mx-auto px-4 py-16">
<h2 className="text-2xl font-bold text-center mb-12" style={{ color: 'var(--cm-text-primary)' }}>
Why ChronoMind?
</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-6">
{FEATURES.map((feature) => (
<div
key={feature.title}
className="rounded-xl border p-6"
style={{
backgroundColor: 'var(--cm-surface-card)',
borderColor: 'var(--cm-border)',
}}
>
<div className="mb-4" style={{ color: 'var(--cm-accent)' }}>
{feature.icon}
</div>
<h3 className="text-base font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
{feature.title}
</h3>
<p className="text-sm leading-relaxed" style={{ color: 'var(--cm-text-secondary)' }}>
{feature.description}
</p>
</div>
))}
</div>
</section>
{/* Social proof placeholder */}
<section className="max-w-3xl mx-auto px-4 py-16 text-center">
<p className="text-sm" style={{ color: 'var(--cm-text-tertiary)' }}>
Built for people who struggle with time awareness. Especially helpful for ADHD, time blindness,
and anyone who&apos;s ever missed a meeting because &quot;it was only 5 minutes away.&quot;
</p>
</section>
{/* CTA */}
<section className="max-w-3xl mx-auto px-4 py-16 text-center">
<h2 className="text-3xl font-bold mb-4" style={{ color: 'var(--cm-text-primary)' }}>
Ready to take control of your time?
</h2>
<p className="mb-8" style={{ color: 'var(--cm-text-secondary)' }}>
No signup. No credit card. Just better timers.
</p>
<Link
href="/"
className="inline-flex items-center gap-2 px-8 py-3.5 rounded-xl text-base font-semibold"
style={{ backgroundColor: 'var(--cm-accent)', color: '#fff' }}
>
Launch ChronoMind <ArrowRight size={16} />
</Link>
</section>
{/* Footer */}
<footer className="max-w-5xl mx-auto px-4 py-8 border-t flex items-center justify-between text-xs"
style={{ borderColor: 'var(--cm-border)', color: 'var(--cm-text-tertiary)' }}
>
<span>&copy; 2026 ChronoMind</span>
<div className="flex gap-4">
<Link href="/privacy">Privacy</Link>
<Link href="/terms">Terms</Link>
<a href="https://github.com/saravanakumardb1/learning_ai_clock" target="_blank" rel="noopener noreferrer">GitHub</a>
</div>
</footer>
</div>
);
}

View File

@ -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({

View File

@ -0,0 +1,100 @@
import Link from 'next/link';
export default function PrivacyPage() {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
<div className="max-w-2xl mx-auto px-4 py-12">
<Link href="/" className="text-sm mb-8 inline-block" style={{ color: 'var(--cm-accent)' }}>
Back to ChronoMind
</Link>
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
Privacy Policy
</h1>
<p className="text-sm mb-8" style={{ color: 'var(--cm-text-tertiary)' }}>
Last updated: February 2026
</p>
<div className="space-y-6 text-sm leading-relaxed" style={{ color: 'var(--cm-text-secondary)' }}>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
1. What We Collect
</h2>
<p>
<strong>Nothing.</strong> ChronoMind stores all your data locally in your browser using IndexedDB.
No data is sent to any server. No accounts are required.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
2. Local Storage
</h2>
<p>
Your timers, routines, categories, and preferences are stored locally in your browser&apos;s IndexedDB.
This data never leaves your device. Clearing your browser data will delete all ChronoMind data.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
3. Analytics
</h2>
<p>
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.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
4. Notifications
</h2>
<p>
ChronoMind requests notification permission to send pre-warning alerts. This is entirely optional
and can be revoked in your browser settings at any time.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
5. Cloud Sync (Future)
</h2>
<p>
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.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
6. Third Parties
</h2>
<p>
ChronoMind does not share any data with third parties. There are no ads, no tracking pixels,
and no data brokers.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
7. Contact
</h2>
<p>
Questions about this policy? Open an issue on our{' '}
<a
href="https://github.com/saravanakumardb1/learning_ai_clock"
className="underline"
style={{ color: 'var(--cm-accent)' }}
target="_blank"
rel="noopener noreferrer"
>
GitHub repository
</a>.
</p>
</section>
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,84 @@
import Link from 'next/link';
export default function TermsPage() {
return (
<div className="min-h-screen" style={{ backgroundColor: 'var(--cm-bg-canvas)' }}>
<div className="max-w-2xl mx-auto px-4 py-12">
<Link href="/" className="text-sm mb-8 inline-block" style={{ color: 'var(--cm-accent)' }}>
Back to ChronoMind
</Link>
<h1 className="text-3xl font-bold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
Terms of Service
</h1>
<p className="text-sm mb-8" style={{ color: 'var(--cm-text-tertiary)' }}>
Last updated: February 2026
</p>
<div className="space-y-6 text-sm leading-relaxed" style={{ color: 'var(--cm-text-secondary)' }}>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
1. Acceptance
</h2>
<p>
By using ChronoMind, you agree to these terms. ChronoMind is provided as-is, free of charge
for personal use.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
2. Description
</h2>
<p>
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.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
3. Data Ownership
</h2>
<p>
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.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
4. Limitations
</h2>
<p>
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.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
5. Disclaimer
</h2>
<p>
ChronoMind is provided &quot;as is&quot; without warranty of any kind. We are not liable for
any missed timers, late notifications, or any damages arising from use of the application.
</p>
</section>
<section>
<h2 className="text-lg font-semibold mb-2" style={{ color: 'var(--cm-text-primary)' }}>
6. Changes
</h2>
<p>
We may update these terms as the product evolves. Continued use constitutes acceptance
of updated terms.
</p>
</section>
</div>
</div>
</div>
);
}

View File

@ -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$/);
});
});
});