feat: add PWA manifest, landing page, privacy/terms pages, format tests (53 tests)
This commit is contained in:
parent
6b46384304
commit
ace036b1fc
29
web/public/manifest.json
Normal file
29
web/public/manifest.json
Normal 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"]
|
||||
}
|
||||
176
web/src/app/landing/page.tsx
Normal file
176
web/src/app/landing/page.tsx
Normal 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 · No signup · 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's ever missed a meeting because "it was only 5 minutes away."
|
||||
</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>© 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>
|
||||
);
|
||||
}
|
||||
@ -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({
|
||||
|
||||
100
web/src/app/privacy/page.tsx
Normal file
100
web/src/app/privacy/page.tsx
Normal 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'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>
|
||||
);
|
||||
}
|
||||
84
web/src/app/terms/page.tsx
Normal file
84
web/src/app/terms/page.tsx
Normal 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 "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.
|
||||
</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>
|
||||
);
|
||||
}
|
||||
67
web/src/lib/format.test.ts
Normal file
67
web/src/lib/format.test.ts
Normal 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$/);
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user