Skip to content
Merged
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
6 changes: 6 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,9 @@ Thumbs.db
*.log
*.tmp
*.temp

# Playwright
/playwright-screenshots
/playwright-report
/playwright-artifacts
/test-results
34 changes: 34 additions & 0 deletions e2e/screenshots.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { test } from '@playwright/test';
import path from 'path';

const pages = [
{ name: 'home', route: '/' },
{ name: 'about', route: '/about' },
{ name: 'projects', route: '/projects' },
{ name: 'services', route: '/services' },
{ name: 'contact', route: '/contact' },
{ name: 'sitemap', route: '/sitemap' },
{ name: 'updates', route: '/updates' },
];

const screenshotDir = path.join(__dirname, '..', 'playwright-screenshots');

for (const pg of pages) {
test(`screenshot: ${pg.name}`, async ({ page, browserName }, testInfo) => {
const viewport = testInfo.project.name.includes('iPhone') ? 'mobile' : 'desktop';

// The app loads from /react.html and uses ?redirect= for client-side routing
const url = pg.route === '/'
? '/react.html'
: `/react.html?redirect=${pg.route}`;

await page.goto(url, { waitUntil: 'networkidle' });
// Allow animations and lazy-loaded content to settle
await page.waitForTimeout(3000);

await page.screenshot({
path: path.join(screenshotDir, `${pg.name}-${viewport}-${browserName}.png`),
fullPage: true,
});
});
}
64 changes: 64 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
"test": "vitest",
"test:ui": "vitest --ui",
"test:coverage": "vitest --coverage",
"test:e2e": "npx playwright test",
"test:screenshots": "npx playwright test e2e/screenshots.spec.ts",
"update": "node scripts/portfolio-update.js",
"update:quick": "echo '$(date): Quick update' >> devlog.md && git add . && git commit -m 'Portfolio: Quick update' && git push",
"update:stats": "node scripts/generate-stats.js"
Expand All @@ -46,6 +48,7 @@
"license": "ISC",
"devDependencies": {
"@eslint/js": "^9.31.0",
"@playwright/test": "^1.58.2",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
Expand Down
38 changes: 38 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
testDir: './e2e',
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: 0,
workers: 1,
reporter: 'html',
use: {
baseURL: 'http://localhost:4173',
screenshot: 'off', // We take screenshots manually in tests
},
projects: [
{
name: 'Desktop Chrome',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'Desktop Firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'Desktop Safari',
use: { ...devices['Desktop Safari'] },
},
{
name: 'iPhone 14',
use: { ...devices['iPhone 14'] },
},
],
webServer: {
command: 'npm run build && npm run preview -- --port 4173',
url: 'http://localhost:4173',
reuseExistingServer: !process.env.CI,
timeout: 60000,
},
});
84 changes: 32 additions & 52 deletions src/components/Header.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,14 @@
left: 0;
right: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.95);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
border-bottom: 1px solid var(--matrix-green, #00ff00);
background: #000000;
border-bottom: 1px solid rgba(0, 255, 0, 0.2);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.siteHeader.scrolled {
background: rgba(0, 0, 0, 0.98);
box-shadow: 0 2px 20px rgba(0, 255, 0, 0.1);
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
border-bottom-color: rgba(0, 255, 0, 0.3);
}

.container {
Expand Down Expand Up @@ -161,16 +159,16 @@
.nav {
display: flex;
align-items: center;
gap: 2.5rem; /* Increased for better spacing after header name */
margin-left: 1rem; /* Shift right to accommodate // symbols */
gap: 1.5rem;
}

.navList {
display: flex;
list-style: none;
margin: 0;
padding: 0;
gap: 2rem;
gap: 1.5rem;
align-items: center;
}

.navItem {
Expand All @@ -186,13 +184,13 @@

.navLink {
font-family: var(--font-body);
font-size: 0.75rem; /* Reduced from 0.875rem */
color: rgba(0, 255, 0, 0.7);
font-size: 0.8rem;
color: rgba(255, 255, 255, 0.6);
text-decoration: none;
text-transform: uppercase;
letter-spacing: 0.1em;
padding: 0.5rem 0.75rem;
transition: color 0.3s ease, text-shadow 0.3s ease;
letter-spacing: 0.12em;
padding: 0.4rem 0;
transition: color 0.3s ease;
display: flex;
align-items: center;
gap: 0.5rem;
Expand All @@ -203,25 +201,22 @@
.navLink::after {
content: '';
position: absolute;
bottom: 0.25rem;
left: 50%;
bottom: 0;
left: 0;
width: 0;
height: 2px;
height: 1px;
background: var(--matrix-green, #00ff00);
box-shadow: 0 0 8px rgba(0, 255, 0, 0.8);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateX(-50%);
transition: width 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}

.navLink:hover,
.navLink.active {
color: var(--matrix-green, #00ff00);
text-shadow: 0 0 10px rgba(0, 255, 0, 0.5);
color: #ffffff;
}

.navLink:hover::after,
.navLink.active::after {
width: 85%;
width: 100%;
}

.navIcon {
Expand All @@ -244,18 +239,19 @@
visibility: visible !important;
flex-direction: row !important;
padding: 0 !important;
gap: 2rem !important;
gap: 1.5rem !important;
border: none !important;
margin-top: 10px;
margin: 0 !important;
align-items: center;
}

.menuToggle {
display: none !important;
}

.navLink {
color: rgba(0, 255, 0, 0.8) !important;
font-weight: 500 !important;
color: rgba(255, 255, 255, 0.6) !important;
font-weight: 400 !important;
}
}

Expand Down Expand Up @@ -333,14 +329,6 @@
.nav {
align-items: center;
}

/* Ensure theme toggle aligns perfectly with nav items */
.nav > :last-child {
margin-left: 0.5rem;
align-self: center;
display: flex;
align-items: center;
}
}

/* Matrix rain effect - only on logo hover */
Expand Down Expand Up @@ -548,18 +536,18 @@
}

[data-theme="dark"] .siteHeader {
background: rgba(0, 0, 0, 0.95);
border-bottom: 1px solid rgba(74, 158, 255, 0.5) !important; /* Blue border */
background: #000000;
border-bottom: 1px solid rgba(74, 158, 255, 0.2) !important;
}

[data-theme="dark"] .siteHeader.scrolled {
background: rgba(0, 0, 0, 0.98);
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.5);
box-shadow: 0 2px 20px rgba(0, 0, 0, 0.5);
border-bottom-color: rgba(74, 158, 255, 0.3) !important;
}

[data-theme="dark"] .headerTitle h1 {
color: #4A9EFF !important; /* Blue to match nav links */
text-shadow: 0 0 10px rgba(74, 158, 255, 0.3);
color: #4A9EFF !important;
text-shadow: none;
font-weight: 500;
}

Expand All @@ -577,18 +565,12 @@
}

[data-theme="dark"] .navLink {
color: #4A9EFF; /* Blue nav links */
font-weight: 400;
}

[data-theme="dark"] .navLink:hover {
color: #5AADFF; /* Lighter blue on hover */
text-shadow: 0 0 10px rgba(74, 158, 255, 0.3);
background: none !important; /* Force remove background in dark mode */
color: rgba(255, 255, 255, 0.6);
}

[data-theme="dark"] .navLink:hover,
[data-theme="dark"] .navLink.active {
color: #5AADFF; /* Blue active state */
color: #ffffff;
}

[data-theme="dark"] .menuToggle span {
Expand Down Expand Up @@ -618,7 +600,5 @@

/* Hardware acceleration for smooth animations */
.siteHeader {
will-change: transform;
transform: translateZ(0);
-webkit-transform: translateZ(0);
}
Loading
Loading