A modern, feature-rich countdown timer and event tracking application built with Svelte 5, Vite, and Firebase. Create personal timers with audio alerts, plan events with live countdowns, and share events with others via shareable links.
/e/{8-char-id})┌─────────────────────────────────────────────────────────────┐
│ Countdown Timer App │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ UI Layer (Svelte Components) │ │
│ │ - Header, TabNav, TimerCard, EventCard │ │
│ │ - EventForm, MigrationModal, SharedEventPage │ │
│ └────────────────────────────────────────────────────────┘ │
│ ▲ │
│ │ │
│ ┌────────────────────────────────────────────────────────┐ │
│ │ State Management (Svelte Stores) │ │
│ │ - timers.js (CRUD + sync) │ │
│ │ - events.js (sharing + linked copies) │ │
│ │ - auth.js (Firebase auth state) │ │
│ │ - settings.js (theme, volume, sound) │ │
│ └────────────────────────────────────────────────────────┘ │
│ ▲ ▲ │
│ │ │ │
│ ┌─────────────────────┐ ┌──────────────────────────┐ │
│ │ localStorage │ │ Firebase (config, sync) │ │
│ │ (Offline cache) │ │ - Authentication │ │
│ │ │ │ - Firestore CRUD │ │
│ └─────────────────────┘ │ - Offline persistence │ │
│ │ - Batch migration │ │
│ └──────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
LOCAL MODE (Signed out):
User Action → Store (update state) → localStorage (persist)
CLOUD MODE (Signed in):
User Action → Store (update state) → localStorage (persist)
│
├─→ Firestore (sync)
│ (real-time listener)
│
← ← ← ← ← ←
SHARED EVENT (Public link):
Shared URL (/e/{shareId})
│
├─→ Look up shareLink collection
│
└─→ Fetch event details
│
├─→ Display countdown
│
└─→ (Optional) Add to my account
countdown-timer-svelte/
├── src/
│ ├── App.svelte # Root component, routing logic
│ ├── main.js # Entry point, mount target
│ ├── app.css # Global styles, CSS variables
│ │
│ └── lib/
│ ├── stores/
│ │ ├── auth.js # Firebase auth, user state
│ │ ├── timers.js # Timer CRUD + cloud sync
│ │ ├── events.js # Event CRUD + sharing
│ │ └── settings.js # Theme, volume, sound settings
│ │
│ ├── components/
│ │ ├── Header.svelte # Logo, user menu, auth button
│ │ ├── TabNav.svelte # Timers/Events tab switcher
│ │ ├── TimerCard.svelte # Single timer with controls
│ │ ├── TimersList.svelte # Grid of timers + add button
│ │ ├── EventCard.svelte # Event countdown + share button
│ │ ├── EventsList.svelte # List of events + form modal
│ │ ├── EventForm.svelte # Add/edit event modal
│ │ ├── SharedEventPage.svelte # /e/{shareId} public page
│ │ ├── MigrationModal.svelte # Local→cloud data merge
│ │ ├── AuthButton.svelte # Sign in/out button
│ │ ├── UserMenu.svelte # User profile dropdown
│ │ └── Footer.svelte # App footer
│ │
│ └── firebase/
│ ├── config.js # Firebase init + persistence
│ └── sync.js # Firestore CRUD + migration
│
├── package.json
├── vite.config.js
├── svelte.config.js
├── index.html
└── .env.local # Firebase credentials (git ignored)
User clicks "Sign in with Google"
│
├─→ signInWithRedirect() (auth/google)
│
├─→ Browser redirects to Google OAuth
│ (User grants permissions)
│
├─→ Redirect back to app
│
├─→ handleRedirectResult()
│ (Auth store updates with user)
│
├─→ Create/update user document in Firestore
│ (uid, email, displayName, settings)
│
└─→ Subscribe to Firestore collections
(timers, events, settings in real-time)
Auth State Management:
authStore: Main store with { user, loading, initialized, error }isAuthenticated: Derived store, boolean user presencecurrentUser: Derived store, user object or nullShare IDs are 8-character random strings (alphanumeric, 52^8 combinations ≈ 53 trillion):
Event Owner creates event:
event = {
id: "123-abc...",
name: "Birthday Party",
targetDate: 1735689600000,
ownerId: "user123",
isShared: false,
shareId: null
}
Owner clicks "Share":
1. Generate shareId: "aB3cD9eF"
2. Update event: isShared=true, shareId="aB3cD9eF"
3. Create shareLink doc in Firestore:
{
eventId: "123-abc...",
ownerId: "user123",
createdAt: serverTimestamp()
}
4. Share URL: https://app.com/e/aB3cD9eF
Others receive link and visit /e/aB3cD9eF:
1. Look up shareLink[aB3cD9eF]
2. Get eventId from shareLink
3. Fetch event from events/{eventId}
4. Display read-only countdown
5. (Optional) Add linked copy to their account:
- Creates new event in their ownership
- Sets isLinkedCopy=true, originalEventId=original
- Tracks original owner for UI attribution
Linked copies allow followers to independently track the event. If original owner deletes event, linked copies are marked isOrphaned=true but remain visible.
Dynamic CSS variables with system preference detection:
// Light mode (default)
main {
--bg-primary: #f5f5f5;
--bg-card: #ffffff;
--text-primary: #333333;
--text-secondary: #666666;
--border-color: #e0e0e0;
}
// Dark mode
main.dark {
--bg-primary: #1a1a2e;
--bg-card: #16213e;
--text-primary: #eaeaea;
--text-secondary: #b8b8b8;
--border-color: #2a2a4a;
}
Theme detection:
prefers-color-scheme media queryEvery store follows this pattern:
Initialization:
1. Load from localStorage
2. Set writable store with local data
Auth subscription:
if (user signs in):
- Subscribe to Firestore collection
- Real-time updates merge with local state
- Preserve ephemeral state (running timers)
- Sync local writes to cloud
if (user signs out):
- Unsubscribe from Firestore
- Reload from localStorage
- Fall back to offline-only mode
Persistence:
- Every store write saves to localStorage (fallback)
- Cloud sync is best-effort (network errors caught)
- Running/paused timer states NOT synced (ephemeral)
IDLE → START → RUNNING → (PAUSE → PAUSED → START)
│ │
│ └─→ TIME_EXPIRES → COMPLETED
│
├─→ RESET → IDLE
└─→ DELETE (removed)
Persistent Properties (synced to cloud):
- id, name, duration, remaining
Ephemeral Properties (local only):
- status (running/paused/completed/idle)
- endTime (calculated from now + remaining)
- displayTime (calculated in real-time)
Clock tick interval: 100ms (every 100ms check remaining time)
Timer:
{
id: "1733000000000-a1b2c3d4e", // timestamp-random
name: "Workout",
duration: 1800, // seconds
remaining: 1800,
status: "idle" | "running" | "paused" | "completed",
endTime: null | 1733000001800, // Date.now() + remaining*1000
// Cloud metadata (from Firestore)
_cloud?: {
hours: 0,
minutes: 30,
seconds: 0,
label: "Workout",
createdAt: 1733000000000,
updatedAt: 1733000000000
}
}
Event:
{
id: "1733000000000-a1b2c3d4e",
name: "Conference Keynote",
targetDate: 1735689600000, // when countdown reaches 0
createdAt: 1733000000000,
updatedAt: 1733000000000,
// Ownership
ownerId: "firebase_uid_123",
ownerDisplayName: "Alice",
ownerEmail: "[email protected]",
// Sharing
isShared: false,
shareId: "aB3cD9eF" | null,
visibility: "private" | "public",
// Linked copy tracking
isLinkedCopy: false, // true if added from shared link
originalEventId: null | "...", // original event id
originalOwnerId: null | "...", // original owner uid
isOrphaned: false // true if original deleted
}
User (Firestore):
{
uid: "firebase_uid_123",
email: "[email protected]",
displayName: "Alice",
photoURL: "https://...",
createdAt: Timestamp,
lastLoginAt: Timestamp,
settings: {
soundEnabled: true,
volume: 70,
themePreference: "system" | "light" | "dark",
vibrationEnabled: true,
keepScreenAwake: false
}
}
Create .env.local in project root:
VITE_FIREBASE_API_KEY=your_api_key
VITE_FIREBASE_AUTH_DOMAIN=your-project.firebaseapp.com
VITE_FIREBASE_PROJECT_ID=your-project-id
VITE_FIREBASE_STORAGE_BUCKET=your-project.appspot.com
VITE_FIREBASE_MESSAGING_SENDER_ID=your_sender_id
VITE_FIREBASE_APP_ID=your_app_id
Important: Never commit .env.local to version control. Add to .gitignore:
.env.local
.env.*.local
node_modules/
# Install dependencies
npm install
# Start dev server (http://localhost:5173)
npm run dev
# Build for production
npm run build
# Preview production build locally
npm run preview
Replace default Firestore rules with (in Firebase Console):
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Users can only read/write their own user document
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
// User's timers (subcollection)
match /timers/{timerId} {
allow read, write: if request.auth.uid == userId;
}
// User's event references (subcollection)
match /eventRefs/{eventRefId} {
allow read, write: if request.auth.uid == userId;
}
}
// Events are readable by owner or if public
match /events/{eventId} {
allow read: if resource.data.visibility == 'public' ||
request.auth.uid == resource.data.ownerId;
allow write: if request.auth.uid == resource.data.ownerId;
allow delete: if request.auth.uid == resource.data.ownerId;
}
// Share links map share IDs to events
match /shareLinks/{shareId} {
allow read: if true; // Anyone can look up a share link
allow write: if request.auth != null; // Only authenticated users create
allow delete: if request.auth.uid == resource.data.ownerId;
}
}
}
In Firebase Console → Authentication → Settings:
npm run dev
Vite dev server at http://localhost:5173 with:
npm run build
Outputs optimized bundle to dist/:
# Install Firebase CLI
npm install -g firebase-tools
# Authenticate
firebase login
# Deploy
firebase deploy
This deploys:
Works with any static host (Vercel, Netlify, GitHub Pages):
npm run build
# Deploy dist/ folder to your host
| File | Purpose |
|---|---|
src/App.svelte |
Root layout, route parsing, migration modal |
src/lib/stores/auth.js |
Firebase auth setup, user state, sign-in/out |
src/lib/stores/timers.js |
Timer CRUD, cloud sync, offline persistence |
src/lib/stores/events.js |
Event CRUD, sharing, linked copies |
src/lib/stores/settings.js |
Theme, volume, sound preferences |
src/lib/firebase/config.js |
Firebase init, persistence config |
src/lib/firebase/sync.js |
Firestore queries, batch migration, helpers |
src/lib/components/TimerCard.svelte |
Timer UI, progress circle, alarm beep |
src/lib/components/EventCard.svelte |
Event countdown, share modal, delete |
src/lib/components/SharedEventPage.svelte |
Public event page, add to account |
src/lib/components/MigrationModal.svelte |
Local→cloud data migration prompt |
Requires:
If you see "Firebase is not configured" errors:
.env.local has all required variablesnpm run dev)Share links are public URLs like https://yoursite.com/e/aB3cD9eF:
shareLinks"public"If timers don't appear on other devices:
app.css for CSS variable definitionsmain.dark class is applied to rootThis project demonstrates modern web patterns:
Feel free to fork, extend, and deploy!
Built with Svelte 5 + Firebase. No external UI frameworks, pure component-driven design.