Skip to content

Commit 56fedb3

Browse files
Raushan kumarclaude
andcommitted
Implement high priority production improvements
Performance: - Add code splitting with React.lazy for all feature components - Lazy load Dashboard, ChatBot, and all calculators/trackers PWA Support: - Add manifest.json with app metadata and shortcuts - Add service worker for offline caching - Add PWA meta tags for iOS and Android Accessibility: - Add SkipLink component for keyboard navigation - Fix focus management in EditTodoModal with focus trap - Add ARIA labels and roles throughout - Add proper label associations for form inputs Error Handling: - Add retry logic with exponential backoff for Firestore ops - Add useOnlineStatus hook for network detection - Add OfflineIndicator component - Return error state from all Firestore hooks Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 7764c86 commit 56fedb3

10 files changed

Lines changed: 411 additions & 94 deletions

File tree

index.html

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,53 @@
22
<html lang="en">
33
<head>
44
<meta charset="UTF-8" />
5-
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
65
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<meta name="description" content="Flowly - Your all-in-one productivity workspace for tasks, attendance, leaves, expenses, focus timer, and more" />
8-
<meta name="theme-color" content="#7c3aed" />
6+
7+
<!-- Primary Meta Tags -->
8+
<title>Flowly - Productivity Workspace</title>
9+
<meta name="title" content="Flowly - Productivity Workspace" />
10+
<meta name="description" content="Your all-in-one productivity workspace for tasks, attendance, leaves, expenses, focus timer, and more" />
911
<meta name="application-name" content="Flowly" />
12+
<meta name="author" content="Flowly" />
13+
14+
<!-- PWA Meta Tags -->
15+
<meta name="theme-color" content="#7c3aed" />
16+
<meta name="mobile-web-app-capable" content="yes" />
17+
<meta name="apple-mobile-web-app-capable" content="yes" />
18+
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
19+
<meta name="apple-mobile-web-app-title" content="Flowly" />
20+
21+
<!-- Icons -->
22+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
23+
<link rel="apple-touch-icon" href="/favicon.svg" />
24+
<link rel="manifest" href="/manifest.json" />
25+
26+
<!-- Open Graph / Facebook -->
27+
<meta property="og:type" content="website" />
28+
<meta property="og:url" content="https://flowly.app/" />
1029
<meta property="og:title" content="Flowly - Productivity Workspace" />
1130
<meta property="og:description" content="Your all-in-one productivity workspace for tasks, attendance, leaves, expenses, focus timer, and more" />
12-
<meta property="og:type" content="website" />
13-
<title>Flowly - Productivity Workspace</title>
31+
<meta property="og:image" content="/og-image.png" />
32+
33+
<!-- Twitter -->
34+
<meta property="twitter:card" content="summary_large_image" />
35+
<meta property="twitter:url" content="https://flowly.app/" />
36+
<meta property="twitter:title" content="Flowly - Productivity Workspace" />
37+
<meta property="twitter:description" content="Your all-in-one productivity workspace for tasks, attendance, leaves, expenses, focus timer, and more" />
38+
<meta property="twitter:image" content="/og-image.png" />
1439
</head>
1540
<body>
1641
<div id="root"></div>
1742
<script type="module" src="/src/main.jsx"></script>
43+
<!-- Register service worker -->
44+
<script>
45+
if ('serviceWorker' in navigator) {
46+
window.addEventListener('load', () => {
47+
navigator.serviceWorker.register('/sw.js').catch(() => {
48+
// Service worker registration failed silently
49+
});
50+
});
51+
}
52+
</script>
1853
</body>
1954
</html>

public/manifest.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
{
2+
"name": "Flowly - Productivity Workspace",
3+
"short_name": "Flowly",
4+
"description": "Your all-in-one productivity workspace for tasks, attendance, leaves, expenses, focus timer, and more",
5+
"start_url": "/",
6+
"display": "standalone",
7+
"background_color": "#1e1b4b",
8+
"theme_color": "#7c3aed",
9+
"orientation": "portrait-primary",
10+
"icons": [
11+
{
12+
"src": "/favicon.svg",
13+
"sizes": "any",
14+
"type": "image/svg+xml",
15+
"purpose": "any maskable"
16+
},
17+
{
18+
"src": "/icon-192.png",
19+
"sizes": "192x192",
20+
"type": "image/png"
21+
},
22+
{
23+
"src": "/icon-512.png",
24+
"sizes": "512x512",
25+
"type": "image/png"
26+
}
27+
],
28+
"categories": ["productivity", "utilities", "business"],
29+
"screenshots": [],
30+
"shortcuts": [
31+
{
32+
"name": "Dashboard",
33+
"url": "/?view=dashboard",
34+
"description": "View your dashboard"
35+
},
36+
{
37+
"name": "Tasks",
38+
"url": "/?view=tasks",
39+
"description": "Manage your tasks"
40+
},
41+
{
42+
"name": "Focus Timer",
43+
"url": "/?view=focus",
44+
"description": "Start a focus session"
45+
}
46+
]
47+
}

public/sw.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
const CACHE_NAME = 'flowly-v1';
2+
const STATIC_ASSETS = [
3+
'/',
4+
'/favicon.svg',
5+
'/manifest.json'
6+
];
7+
8+
// Install event - cache static assets
9+
self.addEventListener('install', (event) => {
10+
event.waitUntil(
11+
caches.open(CACHE_NAME).then((cache) => {
12+
return cache.addAll(STATIC_ASSETS);
13+
})
14+
);
15+
self.skipWaiting();
16+
});
17+
18+
// Activate event - clean up old caches
19+
self.addEventListener('activate', (event) => {
20+
event.waitUntil(
21+
caches.keys().then((cacheNames) => {
22+
return Promise.all(
23+
cacheNames
24+
.filter((name) => name !== CACHE_NAME)
25+
.map((name) => caches.delete(name))
26+
);
27+
})
28+
);
29+
self.clients.claim();
30+
});
31+
32+
// Fetch event - network first, fallback to cache
33+
self.addEventListener('fetch', (event) => {
34+
// Skip non-GET requests
35+
if (event.request.method !== 'GET') return;
36+
37+
// Skip Firebase and external API requests
38+
const url = new URL(event.request.url);
39+
if (
40+
url.hostname.includes('firebase') ||
41+
url.hostname.includes('googleapis') ||
42+
url.hostname.includes('google.com')
43+
) {
44+
return;
45+
}
46+
47+
event.respondWith(
48+
fetch(event.request)
49+
.then((response) => {
50+
// Clone the response before caching
51+
const responseClone = response.clone();
52+
caches.open(CACHE_NAME).then((cache) => {
53+
cache.put(event.request, responseClone);
54+
});
55+
return response;
56+
})
57+
.catch(() => {
58+
// Fallback to cache if network fails
59+
return caches.match(event.request).then((cachedResponse) => {
60+
if (cachedResponse) {
61+
return cachedResponse;
62+
}
63+
// Return offline page for navigation requests
64+
if (event.request.mode === 'navigate') {
65+
return caches.match('/');
66+
}
67+
return new Response('Offline', { status: 503 });
68+
});
69+
})
70+
);
71+
});

src/App.jsx

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,58 @@
1-
import { useState } from 'react';
1+
import { useState, lazy, Suspense } from 'react';
22
import { AuthProvider, useAuth } from './contexts/AuthContext';
33
import { NavigationProvider, useNavigation } from './contexts/NavigationContext';
44
import { GamificationProvider } from './contexts/GamificationContext';
55
import { ErrorBoundary } from './components/ErrorBoundary';
6+
import { SkipLink } from './components/SkipLink';
7+
import { OfflineIndicator } from './components/OfflineIndicator';
68
import { useTodos } from './hooks/useFirestore';
79
import { formatDateKey, formatDisplayDate, generateId } from './utils/dateUtils';
810

9-
// Navigation components
11+
// Navigation components (always needed)
1012
import { Sidebar } from './components/Navigation/Sidebar';
1113
import { MobileNav } from './components/Navigation/MobileNav';
1214

13-
// Dashboard
14-
import { Dashboard } from './components/Dashboard/Dashboard';
15+
// Auth components (needed early)
16+
import { SignIn } from './components/Auth/SignIn';
17+
import { UserMenu } from './components/Auth/UserMenu';
1518

16-
// Feature components
19+
// Core task components (frequently used)
1720
import { Calendar } from './components/Calendar';
1821
import { TodoList } from './components/TodoList';
1922
import { AddTodo } from './components/AddTodo';
2023
import { EditTodoModal } from './components/EditTodoModal';
21-
import { AttendanceTracker } from './components/AttendanceTracker';
22-
import { SignIn } from './components/Auth/SignIn';
23-
import { UserMenu } from './components/Auth/UserMenu';
2424
import { DailyQuote } from './components/DailyQuote';
25-
import { QuickLinks } from './components/QuickLinks';
26-
import { UpcomingMeetings } from './components/UpcomingMeetings';
27-
import { TripPlanner } from './components/TripPlanner';
28-
import { PomodoroTimer } from './components/PomodoroTimer';
29-
import { TaxCalculator } from './components/TaxCalculator';
30-
import { LeaveTracker } from './components/LeaveTracker';
31-
import { ExpenseTracker } from './components/ExpenseTracker';
32-
import { SalaryCalculator } from './components/SalaryCalculator';
33-
import { Notes } from './components/Notes';
34-
35-
// Gamification
36-
import { LevelProgress } from './components/Gamification/LevelProgress';
37-
38-
// ChatBot
39-
import { ChatBot } from './components/ChatBot/ChatBot';
25+
26+
// Lazy loaded components (loaded on demand)
27+
const Dashboard = lazy(() => import('./components/Dashboard/Dashboard').then(m => ({ default: m.Dashboard })));
28+
const AttendanceTracker = lazy(() => import('./components/AttendanceTracker').then(m => ({ default: m.AttendanceTracker })));
29+
const QuickLinks = lazy(() => import('./components/QuickLinks').then(m => ({ default: m.QuickLinks })));
30+
const UpcomingMeetings = lazy(() => import('./components/UpcomingMeetings').then(m => ({ default: m.UpcomingMeetings })));
31+
const TripPlanner = lazy(() => import('./components/TripPlanner').then(m => ({ default: m.TripPlanner })));
32+
const PomodoroTimer = lazy(() => import('./components/PomodoroTimer').then(m => ({ default: m.PomodoroTimer })));
33+
const TaxCalculator = lazy(() => import('./components/TaxCalculator').then(m => ({ default: m.TaxCalculator })));
34+
const LeaveTracker = lazy(() => import('./components/LeaveTracker').then(m => ({ default: m.LeaveTracker })));
35+
const ExpenseTracker = lazy(() => import('./components/ExpenseTracker').then(m => ({ default: m.ExpenseTracker })));
36+
const SalaryCalculator = lazy(() => import('./components/SalaryCalculator').then(m => ({ default: m.SalaryCalculator })));
37+
const Notes = lazy(() => import('./components/Notes').then(m => ({ default: m.Notes })));
38+
const ChatBot = lazy(() => import('./components/ChatBot/ChatBot').then(m => ({ default: m.ChatBot })));
39+
40+
// Loading spinner component for Suspense fallback
41+
function LoadingSpinner() {
42+
return (
43+
<div className="flex items-center justify-center py-12">
44+
<div className="text-center">
45+
<div className="w-10 h-10 mx-auto mb-3 rounded-xl bg-white/20 flex items-center justify-center animate-pulse">
46+
<svg className="w-5 h-5 text-white animate-spin" fill="none" viewBox="0 0 24 24">
47+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
48+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
49+
</svg>
50+
</div>
51+
<p className="text-white/60 text-sm">Loading...</p>
52+
</div>
53+
</div>
54+
);
55+
}
4056

4157
function MainContent() {
4258
const { activeView, navigate, sidebarCollapsed } = useNavigation();
@@ -238,10 +254,10 @@ function MainContent() {
238254
return (
239255
<>
240256
{/* Main content area */}
241-
<main className="app-content">
257+
<main id="main-content" className="app-content" role="main" tabIndex={-1}>
242258
<div className="min-h-screen py-6 px-4 lg:py-8 lg:px-8">
243259
{/* Decorative background elements */}
244-
<div className="fixed inset-0 overflow-hidden pointer-events-none">
260+
<div className="fixed inset-0 overflow-hidden pointer-events-none" aria-hidden="true">
245261
<div className="absolute -top-40 -right-40 w-80 h-80 bg-white/10 rounded-full blur-3xl" />
246262
<div className="absolute -bottom-40 -left-40 w-80 h-80 bg-white/10 rounded-full blur-3xl" />
247263
</div>
@@ -276,8 +292,10 @@ function MainContent() {
276292
</header>
277293
)}
278294

279-
{/* Page Content */}
280-
<div className="animate-fade-in">{renderContent()}</div>
295+
{/* Page Content with Suspense */}
296+
<Suspense fallback={<LoadingSpinner />}>
297+
<div className="animate-fade-in">{renderContent()}</div>
298+
</Suspense>
281299
</div>
282300
</div>
283301
</main>
@@ -301,24 +319,12 @@ function AppContent() {
301319
// Show loading state
302320
if (authLoading) {
303321
return (
304-
<div className="min-h-screen flex items-center justify-center">
322+
<div className="min-h-screen flex items-center justify-center" role="status" aria-label="Loading application">
305323
<div className="text-center">
306-
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-white/20 backdrop-blur-sm flex items-center justify-center animate-pulse">
307-
<svg
308-
className="w-8 h-8 text-white"
309-
fill="none"
310-
stroke="currentColor"
311-
viewBox="0 0 24 24"
312-
>
313-
<path
314-
strokeLinecap="round"
315-
strokeLinejoin="round"
316-
strokeWidth={1.5}
317-
d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2"
318-
/>
319-
</svg>
324+
<div className="w-16 h-16 mx-auto mb-4 rounded-2xl bg-gradient-to-br from-violet-500 to-purple-600 flex items-center justify-center animate-pulse shadow-lg">
325+
<span className="text-white font-bold text-2xl">F</span>
320326
</div>
321-
<p className="text-white/60">Loading...</p>
327+
<p className="text-white/60">Loading Flowly...</p>
322328
</div>
323329
</div>
324330
);
@@ -339,8 +345,10 @@ function AppContent() {
339345
{/* Main Content */}
340346
<MainContent />
341347

342-
{/* ChatBot */}
343-
<ChatBot />
348+
{/* ChatBot - Lazy loaded */}
349+
<Suspense fallback={null}>
350+
<ChatBot />
351+
</Suspense>
344352
</div>
345353
</GamificationProvider>
346354
</NavigationProvider>
@@ -350,6 +358,8 @@ function AppContent() {
350358
function App() {
351359
return (
352360
<ErrorBoundary>
361+
<SkipLink />
362+
<OfflineIndicator />
353363
<AuthProvider>
354364
<AppContent />
355365
</AuthProvider>

src/components/AddTodo.jsx

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export function AddTodo({ onAdd }) {
1515
};
1616

1717
return (
18-
<form onSubmit={handleSubmit} className="relative">
18+
<form onSubmit={handleSubmit} className="relative" role="search" aria-label="Add new task">
1919
<div
2020
className={`
2121
relative flex items-center gap-3 p-2 pl-4 rounded-2xl
@@ -36,14 +36,17 @@ export function AddTodo({ onAdd }) {
3636
: 'bg-slate-100 text-slate-400'
3737
}
3838
`}
39+
aria-hidden="true"
3940
>
4041
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
4142
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
4243
</svg>
4344
</div>
4445

4546
{/* Input */}
47+
<label htmlFor="new-task-input" className="sr-only">Add a new task</label>
4648
<input
49+
id="new-task-input"
4750
ref={inputRef}
4851
type="text"
4952
value={text}
@@ -52,12 +55,14 @@ export function AddTodo({ onAdd }) {
5255
onBlur={() => setIsFocused(false)}
5356
placeholder="Add a new task..."
5457
className="flex-1 bg-transparent text-slate-700 placeholder-slate-400 text-[15px] focus:outline-none"
58+
aria-describedby={isFocused && text.trim() ? 'add-task-hint' : undefined}
5559
/>
5660

5761
{/* Submit button */}
5862
<button
5963
type="submit"
6064
disabled={!text.trim()}
65+
aria-label={text.trim() ? `Add task: ${text.trim()}` : 'Add task (enter text first)'}
6166
className={`
6267
px-5 py-2.5 rounded-xl font-medium text-sm
6368
transition-all duration-300
@@ -73,7 +78,7 @@ export function AddTodo({ onAdd }) {
7378

7479
{/* Keyboard hint */}
7580
{isFocused && text.trim() && (
76-
<div className="absolute -bottom-6 left-4 text-xs text-slate-400 animate-fade-in">
81+
<div id="add-task-hint" className="absolute -bottom-6 left-4 text-xs text-slate-400 animate-fade-in" aria-live="polite">
7782
Press <kbd className="px-1.5 py-0.5 bg-slate-100 rounded text-slate-500 font-mono">Enter</kbd> to add
7883
</div>
7984
)}

0 commit comments

Comments
 (0)