Skip to content

Commit 62c09e3

Browse files
authored
Merge branch 'main' into fix/landing-page-ui-improvements
2 parents af48066 + 9daf622 commit 62c09e3

57 files changed

Lines changed: 15095 additions & 9405 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.eslintrc.json

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
11
{
2-
"extends": "next/core-web-vitals",
3-
"rules": {
4-
"react/jsx-no-target-blank": "error"
5-
}
6-
}
2+
"extends": ["next/core-web-vitals"]
3+
}

.github/workflows/e2e.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,6 @@ jobs:
4343

4444
- name: Run Playwright tests
4545
run: npx playwright test
46-
4746
- name: Upload Playwright report
4847
uses: actions/upload-artifact@v4
4948
if: always()

.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,14 @@ test-results/
4040

4141
.vercel
4242
.env*
43+
44+
# PWA artifacts
45+
public/sw.js
46+
public/sw.js.map
47+
public/workbox-*.js
48+
public/workbox-*.js.map
49+
public/fallback-*.js
50+
public/worker-*.js
51+
public/swe-worker-*.js
52+
worker/
53+

e2e/auth-bypass.spec.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,13 +72,13 @@ test("multiple attacker-controlled cookies combined do not bypass authentication
7272
secure: false,
7373
},
7474
{
75-
name: "next-auth.session-token",
75+
name: "__Secure-next-auth.session-token",
7676
value: "forged-token",
7777
domain: "127.0.0.1",
7878
path: "/",
79-
httpOnly: true,
79+
httpOnly: false,
8080
sameSite: "Lax",
81-
secure: false,
81+
secure: true,
8282
},
8383
]);
8484

e2e/dashboard-widgets.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ test.beforeEach(async ({ page }) => {
55
// Create a valid NextAuth JWT and set it as the session cookie so
66
// dashboard pages render as an authenticated user in Playwright.
77
const token = await encode({
8-
secret: process.env.NEXTAUTH_SECRET ?? "playwright-placeholder-secret-that-is-long-enough",
8+
secret: process.env.NEXTAUTH_SECRET || "playwright-placeholder-secret-that-is-long-enough",
99
token: {
1010
name: "Playwright User",
1111
email: "playwright@example.com",

e2e/landing.spec.js

Lines changed: 7 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,28 @@
11
import { expect, test } from "@playwright/test";
22

3-
test("landing page renders GitHub sign-in entrypoint", async ({ page }) => {
3+
test("[Landing E2E] page renders GitHub sign-in entrypoint", async ({ page }) => {
44
await page.goto("/");
5-
6-
// The hero h1 is "YOUR CODE HAS A PULSE" — verify the page loaded
75
await expect(page.getByRole("heading", { level: 1 })).toBeVisible();
8-
9-
// Two "Sign in with GitHub" links exist (hero + setup section) — check first one
106
await expect(
117
page.getByRole("link", { name: "Sign in with GitHub" }).first(),
12-
).toHaveAttribute("href", /\/auth\/signin/);
13-
14-
// Verify at least one link to the upstream GitHub repo is present
8+
).toHaveAttribute("href", /\/api\/auth\/signin\/github\?callbackUrl=\/dashboard/);
159
await expect(
1610
page.getByRole("link", { name: /star on github/i }).first(),
1711
).toHaveAttribute("href", "https://github.com/Priyanshu-byte-coder/devtrack");
1812
});
1913

20-
test("dashboard stays protected for unauthenticated users", async ({ page }) => {
14+
test("[Landing E2E] dashboard stays protected for unauthenticated users", async ({ page }) => {
2115
await page.goto("/dashboard");
22-
2316
await expect(page).toHaveURL(/\/$/);
2417
await expect(page.getByRole("link", { name: "Sign in with GitHub" }).first()).toBeVisible();
2518
});
2619

27-
test("landing page shows dashboard link", async ({ page }) => {
20+
test("[Landing E2E] landing has dashboard link", async ({ page }) => {
2821
await page.goto("/");
29-
3022
await expect(page.getByRole("link", { name: "Dashboard" })).toBeVisible();
3123
});
3224

33-
test("landing shows footer", async ({ page }) => {
25+
test("[Landing E2E] landing shows footer via test-id", async ({ page }) => {
3426
await page.goto("/");
35-
36-
await expect(
37-
page.locator('[data-testid="landing-footer"]'),
38-
).toBeVisible();
39-
});
27+
await expect(page.locator('[data-testid="landing-footer"]')).toBeVisible();
28+
});

e2e/notifications.spec.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ function mockMetricResponse(url) {
5454

5555
test.beforeEach(async ({ page }) => {
5656
const token = await encode({
57-
secret: process.env.NEXTAUTH_SECRET ?? authSecret,
57+
secret: process.env.NEXTAUTH_SECRET || authSecret,
5858
token: {
5959
name: "Playwright User",
6060
email: "playwright@example.com",

e2e/theme.spec.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,9 @@ import { encode } from "next-auth/jwt";
33

44
test.beforeEach(async ({ page }) => {
55
const token = await encode({
6-
secret: process.env.NEXTAUTH_SECRET ?? "playwright-placeholder-secret-that-is-long-enough",
6+
secret:
7+
process.env.NEXTAUTH_SECRET ||
8+
"playwright-placeholder-secret-that-is-long-enough",
79
token: {
810
name: "Playwright User",
911
email: "playwright@example.com",
@@ -25,7 +27,7 @@ test.beforeEach(async ({ page }) => {
2527
secure: false,
2628
},
2729
]);
28-
30+
2931
await page.route("**/api/user/settings", async (route) => {
3032
await route.fulfill({
3133
contentType: "application/json",
@@ -41,9 +43,9 @@ test("theme toggle switches between dark and light mode", async ({ page }) => {
4143
await expect(themeToggle).toBeVisible();
4244

4345
const initialPressed = await themeToggle.getAttribute("aria-pressed");
44-
46+
4547
await themeToggle.click();
46-
48+
4749
await expect(themeToggle).toHaveAttribute(
4850
"aria-pressed",
4951
initialPressed === "true" ? "false" : "true"

next.config.mjs

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,128 @@
1+
import withPWAInit from "next-pwa";
2+
3+
const withPWA = withPWAInit({
4+
dest: "public",
5+
disable: process.env.NODE_ENV === "development",
6+
register: true,
7+
reloadOnOnline: false,
8+
skipWaiting: true,
9+
fallbacks: {
10+
document: "/offline.html",
11+
},
12+
runtimeCaching: [
13+
{
14+
urlPattern: /^https:\/\/api\.github\.com\/.*$/,
15+
handler: "NetworkFirst",
16+
options: {
17+
cacheName: "github-api-cache",
18+
expiration: {
19+
maxEntries: 100,
20+
maxAgeSeconds: 24 * 60 * 60, // 24 hours
21+
},
22+
},
23+
},
24+
{
25+
urlPattern: ({ url }) => {
26+
if (url.origin !== self.location.origin) return false;
27+
return (
28+
url.pathname === "/api/dashboard" ||
29+
url.pathname === "/api/goals" ||
30+
url.pathname.startsWith("/api/metrics/") ||
31+
url.pathname.startsWith("/api/streak/")
32+
);
33+
},
34+
handler: "NetworkFirst",
35+
method: "GET",
36+
options: {
37+
cacheName: "dashboard-api-cache",
38+
networkTimeoutSeconds: 5,
39+
cacheableResponse: {
40+
statuses: [200],
41+
},
42+
expiration: {
43+
maxEntries: 80,
44+
maxAgeSeconds: 24 * 60 * 60, // 24 hours
45+
},
46+
},
47+
},
48+
{
49+
urlPattern: ({ url }) => {
50+
return (
51+
url.origin === self.location.origin &&
52+
url.pathname === "/api/goals/sync"
53+
);
54+
},
55+
handler: "NetworkOnly",
56+
method: "POST",
57+
options: {
58+
backgroundSync: {
59+
name: "devtrack-goal-sync-queue",
60+
options: {
61+
maxRetentionTime: 24 * 60, // 24 hours
62+
},
63+
},
64+
},
65+
},
66+
{
67+
urlPattern: ({ url }) => {
68+
if (url.origin !== self.location.origin) return false;
69+
if (url.pathname.startsWith("/api/auth/")) return false;
70+
if (url.pathname.startsWith("/api/webhooks/")) return false;
71+
return url.pathname.startsWith("/api/");
72+
},
73+
handler: "NetworkFirst",
74+
method: "GET",
75+
options: {
76+
cacheName: "api-cache",
77+
networkTimeoutSeconds: 5,
78+
cacheableResponse: {
79+
statuses: [200],
80+
},
81+
expiration: {
82+
maxEntries: 80,
83+
maxAgeSeconds: 24 * 60 * 60, // 24 hours
84+
},
85+
},
86+
},
87+
{
88+
urlPattern: /^https:\/\/fonts\.(?:gstatic|googleapis)\.com\/.*$/i,
89+
handler: "CacheFirst",
90+
options: {
91+
cacheName: "font-assets-cache",
92+
cacheableResponse: {
93+
statuses: [0, 200],
94+
},
95+
expiration: {
96+
maxEntries: 16,
97+
maxAgeSeconds: 24 * 60 * 60, // 24 hours
98+
},
99+
},
100+
},
101+
{
102+
urlPattern: ({ url }) => {
103+
if (url.origin !== self.location.origin) return false;
104+
return (
105+
url.pathname.startsWith("/_next/static/") ||
106+
/\.(?:js|css|woff2?|png|jpg|jpeg|gif|svg|ico|webp|json)$/.test(
107+
url.pathname,
108+
)
109+
);
110+
},
111+
handler: "CacheFirst",
112+
options: {
113+
cacheName: "static-assets-cache",
114+
cacheableResponse: {
115+
statuses: [200],
116+
},
117+
expiration: {
118+
maxEntries: 160,
119+
maxAgeSeconds: 24 * 60 * 60, // 24 hours
120+
},
121+
},
122+
},
123+
],
124+
});
125+
1126
/** @type {import("next").NextConfig} */
2127
const nextConfig = {
3128
typescript: {
@@ -30,4 +155,4 @@ const nextConfig = {
30155
},
31156
};
32157

33-
export default nextConfig;
158+
export default withPWA(nextConfig);

0 commit comments

Comments
 (0)