Skip to content

Commit 5480276

Browse files
authored
feat(pwa): implement Progressive Web App (PWA) (#42)
1 parent ab92ac3 commit 5480276

File tree

6 files changed

+125
-4
lines changed

6 files changed

+125
-4
lines changed

app/[locale]/layout.tsx

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import { Header } from "@/features/layout/Header";
1313
import { Footer } from "@/features/layout/Footer";
1414
import { TailwindIndicator } from "@/components/utils/TailwindIndicator";
1515
import { NextTopLoader } from "@/components/ui/next-top-loader";
16+
import { ServiceWorkerRegistration } from "@/components/pwa/ServiceWorkerRegistration";
1617

1718
import { Providers } from "./providers";
1819

@@ -116,15 +117,26 @@ export default async function RootLayout({ params, children }: RootLayoutProps)
116117
<meta charSet="UTF-8" />
117118
<meta content="width=device-width, initial-scale=1, maximum-scale=1 viewport-fit=cover" name="viewport" />
118119

120+
{/* PWA Meta Tags */}
121+
<meta content="yes" name="apple-mobile-web-app-capable" />
122+
<meta content="default" name="apple-mobile-web-app-status-bar-style" />
123+
<meta content="Workout Cool" name="apple-mobile-web-app-title" />
124+
<meta content="yes" name="mobile-web-app-capable" />
125+
<meta content="#FF5722" name="msapplication-TileColor" />
126+
<meta content="/android-chrome-192x192.png" name="msapplication-TileImage" />
127+
128+
{/* PWA Manifest */}
129+
<link href="/manifest.json" rel="manifest" />
130+
119131
{/* eslint-disable-next-line @next/next/no-page-custom-font */}
120132
<link as="style" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="preload" />
121133

122134
{/* Alternate hreflang for i18n */}
123135
<link href="https://www.workout.cool/fr" hrefLang="fr" rel="alternate" />
124136
<link href="https://www.workout.cool/en" hrefLang="en" rel="alternate" />
125137

126-
{/* Balise theme-color unique, synchronisée dynamiquement */}
127-
<meta content="#f3f4f6" name="theme-color" />
138+
{/* Theme color for PWA */}
139+
<meta content="#FF5722" name="theme-color" />
128140

129141
{/* TODO: maybe add some ads ? */}
130142
<noscript>
@@ -150,6 +162,7 @@ export default async function RootLayout({ params, children }: RootLayoutProps)
150162
suppressHydrationWarning
151163
>
152164
<Providers locale={locale}>
165+
<ServiceWorkerRegistration />
153166
<WorkoutSessionsSynchronizer />
154167
<ThemeSynchronizer />
155168
<NextTopLoader color="#FF5722" delay={100} showSpinner={false} />

middleware.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,7 @@ export async function middleware(request: NextRequest) {
2727
}
2828

2929
export const config = {
30-
matcher: ["/((?!api|static|_next|manifest.json|scripts/pixel.js|favicon.ico|robots.txt|service-worker\\.js|images|icons|sitemap.xml).*)"],
30+
matcher: [
31+
"/((?!api|static|_next|manifest.json|scripts/pixel.js|favicon.ico|robots.txt|service-worker\\.js|sw.js|apple-touch-icon.png|android-chrome-.*\\.png|images|icons|sitemap.xml).*)",
32+
],
3133
};

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "workoutcool",
3-
"version": "0.1.0",
3+
"version": "1.1.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbopack",

public/manifest.json

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
{
2+
"background_color": "#f3f4f6",
3+
"categories": ["health", "fitness", "sports"],
4+
"description": "Your personal workout companion - track workouts, build routines, and stay motivated",
5+
"display": "standalone",
6+
"icons": [
7+
{
8+
"src": "/images/favicon-16x16.png",
9+
"sizes": "16x16",
10+
"type": "image/png"
11+
},
12+
{
13+
"src": "/images/favicon-32x32.png",
14+
"sizes": "32x32",
15+
"type": "image/png"
16+
},
17+
{
18+
"src": "/apple-touch-icon.png",
19+
"sizes": "180x180",
20+
"type": "image/png",
21+
"purpose": "any maskable"
22+
},
23+
{
24+
"src": "/android-chrome-192x192.png",
25+
"sizes": "192x192",
26+
"type": "image/png",
27+
"purpose": "any maskable"
28+
},
29+
{
30+
"src": "/android-chrome-512x512.png",
31+
"sizes": "512x512",
32+
"type": "image/png",
33+
"purpose": "any maskable"
34+
}
35+
],
36+
"lang": "en",
37+
"name": "Workout Cool",
38+
"orientation": "portrait",
39+
"scope": "/",
40+
"short_name": "Workout Cool",
41+
"start_url": "/",
42+
"theme_color": "#FF5722"
43+
}

public/sw.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
const CACHE_NAME = "workout-cool-v1";
2+
const urlsToCache = [
3+
"/",
4+
"/manifest.json",
5+
"/images/favicon-32x32.png",
6+
"/images/favicon-16x16.png",
7+
"/apple-touch-icon.png",
8+
"/android-chrome-192x192.png",
9+
"/android-chrome-512x512.png",
10+
];
11+
12+
// Install event - cache resources
13+
self.addEventListener("install", (event) => {
14+
event.waitUntil(
15+
caches.open(CACHE_NAME).then((cache) => {
16+
return cache.addAll(urlsToCache);
17+
}),
18+
);
19+
});
20+
21+
// Fetch event - serve from cache when offline
22+
self.addEventListener("fetch", (event) => {
23+
event.respondWith(
24+
caches.match(event.request).then((response) => {
25+
return response || fetch(event.request);
26+
}),
27+
);
28+
});
29+
30+
// Activate event - clean up old caches
31+
self.addEventListener("activate", (event) => {
32+
event.waitUntil(
33+
caches.keys().then((cacheNames) => {
34+
return Promise.all(
35+
cacheNames.map((cacheName) => {
36+
if (cacheName !== CACHE_NAME) {
37+
return caches.delete(cacheName);
38+
}
39+
}),
40+
);
41+
}),
42+
);
43+
});
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
"use client";
2+
3+
import { useEffect } from "react";
4+
5+
export function ServiceWorkerRegistration() {
6+
useEffect(() => {
7+
if ("serviceWorker" in navigator) {
8+
navigator.serviceWorker
9+
.register("/sw.js")
10+
.then((registration) => {
11+
console.log("SW registered: ", registration);
12+
})
13+
.catch((registrationError) => {
14+
console.log("SW registration failed: ", registrationError);
15+
});
16+
}
17+
}, []);
18+
19+
return null;
20+
}

0 commit comments

Comments
 (0)