Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,6 @@ jobs:

- name: Run Playwright tests
run: npx playwright test
env:
NEXT_PUBLIC_SUPABASE_URL: ${{ secrets.NEXT_PUBLIC_SUPABASE_URL }}
SUPABASE_SERVICE_ROLE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
NEXTAUTH_SECRET: ${{ secrets.NEXTAUTH_SECRET }}
- name: Upload Playwright report
uses: actions/upload-artifact@v4
if: always()
Expand Down
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,14 @@ test-results/

.vercel
.env*

# PWA artifacts
public/sw.js
public/sw.js.map
public/workbox-*.js
public/workbox-*.js.map
public/fallback-*.js
public/worker-*.js
public/swe-worker-*.js
worker/

2 changes: 1 addition & 1 deletion e2e/dashboard-widgets.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ test.beforeEach(async ({ page }) => {
// Create a valid NextAuth JWT and set it as the session cookie so
// dashboard pages render as an authenticated user in Playwright.
const token = await encode({
secret: process.env.NEXTAUTH_SECRET ?? "playwright-placeholder-secret-that-is-long-enough",
secret: process.env.NEXTAUTH_SECRET || "playwright-placeholder-secret-that-is-long-enough",
token: {
name: "Playwright User",
email: "playwright@example.com",
Expand Down
2 changes: 1 addition & 1 deletion e2e/notifications.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ function mockMetricResponse(url) {

test.beforeEach(async ({ page }) => {
const token = await encode({
secret: process.env.NEXTAUTH_SECRET ?? authSecret,
secret: process.env.NEXTAUTH_SECRET || authSecret,
token: {
name: "Playwright User",
email: "playwright@example.com",
Expand Down
130 changes: 129 additions & 1 deletion next.config.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,133 @@
import withPWAInit from "next-pwa";

const withPWA = withPWAInit({
dest: "public",
disable: process.env.NODE_ENV === "development",
register: true,
reloadOnOnline: false,
skipWaiting: true,
fallbacks: {
document: "/offline.html",
},
runtimeCaching: [
{
urlPattern: /^https:\/\/api\.github\.com\/.*$/,
handler: "NetworkFirst",
options: {
cacheName: "github-api-cache",
expiration: {
maxEntries: 100,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: ({ url }) => {
if (url.origin !== self.location.origin) return false;
return (
url.pathname === "/api/dashboard" ||
url.pathname === "/api/goals" ||
url.pathname.startsWith("/api/metrics/") ||
url.pathname.startsWith("/api/streak/")
);
},
handler: "NetworkFirst",
method: "GET",
options: {
cacheName: "dashboard-api-cache",
networkTimeoutSeconds: 5,
cacheableResponse: {
statuses: [200],
},
expiration: {
maxEntries: 80,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: ({ url }) => {
return (
url.origin === self.location.origin &&
url.pathname === "/api/goals/sync"
);
},
handler: "NetworkOnly",
method: "POST",
options: {
backgroundSync: {
name: "devtrack-goal-sync-queue",
options: {
maxRetentionTime: 24 * 60, // 24 hours
},
},
},
},
{
urlPattern: ({ url }) => {
if (url.origin !== self.location.origin) return false;
if (url.pathname.startsWith("/api/auth/")) return false;
if (url.pathname.startsWith("/api/webhooks/")) return false;
return url.pathname.startsWith("/api/");
},
handler: "NetworkFirst",
method: "GET",
options: {
cacheName: "api-cache",
networkTimeoutSeconds: 5,
cacheableResponse: {
statuses: [200],
},
expiration: {
maxEntries: 80,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: /^https:\/\/fonts\.(?:gstatic|googleapis)\.com\/.*$/i,
handler: "CacheFirst",
options: {
cacheName: "font-assets-cache",
cacheableResponse: {
statuses: [0, 200],
},
expiration: {
maxEntries: 16,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
{
urlPattern: ({ url }) => {
if (url.origin !== self.location.origin) return false;
return (
url.pathname.startsWith("/_next/static/") ||
/\.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico|webp|json)$/.test(
url.pathname,
)
);
},
handler: "CacheFirst",
options: {
cacheName: "static-assets-cache",
cacheableResponse: {
statuses: [200],
},
expiration: {
maxEntries: 160,
maxAgeSeconds: 24 * 60 * 60, // 24 hours
},
},
},
],
});

/** @type {import("next").NextConfig} */
const nextConfig = {
typescript: {
ignoreBuildErrors: true,
},
output: "standalone",
images: {
remotePatterns: [
Expand Down Expand Up @@ -27,4 +155,4 @@ const nextConfig = {
},
};

export default nextConfig;
export default withPWA(nextConfig);
Loading
Loading