landing and portfolio

This commit is contained in:
Ovidiu U
2026-03-13 11:00:39 +00:00
parent 55ae54d2c5
commit 8a755e6361
15 changed files with 1748 additions and 149 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
node_modules/
temporary screenshots/
.DS_Store

8
.idea/deployment.xml generated
View File

@@ -1,12 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="PublishConfigData" serverName="ionos - uovidiu.com" remoteFilesAllowedToDisappearOnAutoupload="false">
<component name="PublishConfigData" serverName="ionos - uovidiu.com" remoteFilesAllowedToDisappearOnAutoupload="false" confirmBeforeUploading="false">
<option name="confirmBeforeUploading" value="false" />
<serverData>
<paths name="ionos - uovidiu.com">
<serverdata>
<mappings>
<mapping deploy="/" local="$PROJECT_DIR$" web="/" />
</mappings>
<excludedPaths>
<excludedPath local="true" path="$PROJECT_DIR$/.claude" />
<excludedPath local="true" path="$PROJECT_DIR$/.idea" />
<excludedPath local="true" path="$PROJECT_DIR$/CLAUDE.MD" />
</excludedPaths>
</serverdata>
</paths>
</serverData>

View File

@@ -30,9 +30,9 @@ Apache is required in production for `.htaccess` rules (`mod_rewrite`, `mod_head
### Screenshots
- Puppeteer is installed locally in `node_modules/puppeteer/`. Chrome cache is at `~/.cache/puppeteer/`.
- **Always screenshot from localhost:** `node screenshot.mjs http://localhost:3000`
- **Always screenshot from Herd:** `node screenshot.mjs` (defaults to `http://uovidiu.test`)
- Screenshots save automatically to `./temporary screenshots/screenshot-N.png` (auto-incremented, never overwritten).
- Optional label suffix: `node screenshot.mjs http://localhost:3000 label` → saves as `screenshot-N-label.png`.
- Optional label suffix: `node screenshot.mjs http://uovidiu.test label` → saves as `screenshot-N-label.png`.
- `screenshot.mjs` lives in the project root. Use it as-is.
- After screenshotting, read the PNG from `temporary screenshots/` with the Read tool — Claude can see and analyse the image directly.
- When comparing, be specific: "heading is 32px but reference shows ~24px", "card gap is 16px but should be 24px".

BIN
css/img.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

@@ -7,44 +7,132 @@
text-decoration: underline;
}
/* — Project card link — */
/* ========================================
PORTFOLIO GRID — 3-col card index
======================================== */
.project-card-link {
display: block;
text-decoration: none;
color: var(--white);
.portfolio-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
.project-card-inner {
/* — Project card — */
.project-card {
display: flex;
flex-direction: column;
background: var(--black);
border: 4px solid var(--black);
box-shadow: 8px 8px 0 var(--black);
color: var(--white);
text-decoration: none;
transition: transform 0.1s, box-shadow 0.1s;
}
.project-card-link:hover .project-card-inner {
.project-card:hover {
transform: translate(-2px, -2px);
box-shadow: 10px 10px 0px var(--black);
box-shadow: 8px 8px 0 var(--accent);
}
.project-card-head {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: 0.5rem;
.project-card--wip {
opacity: 0.45;
pointer-events: none;
}
.project-card-year {
/* — Thumbnail — */
.project-card__thumb {
position: relative;
aspect-ratio: 4 / 3;
background: var(--grey-light);
overflow: hidden;
}
.project-card__thumb img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.project-card__label {
position: absolute;
top: 0;
left: 0;
font-family: var(--mono);
font-size: 0.75rem;
color: var(--grey-text);
font-weight: 700;
color: var(--accent);
background: var(--black);
padding: 0.3rem 0.6rem;
z-index: 1;
letter-spacing: 0.04em;
}
.project-card-cta {
display: inline-block;
/* — Body — */
.project-card__body {
padding: 1.25rem;
display: flex;
flex-direction: column;
flex: 1;
border-top: 2px solid var(--grey-dark);
}
.project-card__title {
font-family: var(--mono);
font-size: 0.8rem;
font-weight: 700;
font-size: 0.9rem;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.02em;
color: var(--white);
margin-bottom: 0.6rem;
line-height: 1.3;
}
.project-card__desc {
font-family: var(--sans);
font-size: 0.85rem;
font-weight: 400;
color: var(--grey-text);
line-height: 1.5;
flex: 1;
}
/* — Footer row — */
.project-card__footer {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 1.25rem;
}
.project-card__year {
font-family: var(--mono);
font-size: 0.65rem;
color: var(--grey-text);
letter-spacing: 0.06em;
}
.project-card__arrow {
display: flex;
align-items: center;
justify-content: center;
width: 2rem;
height: 2rem;
border: 2px solid var(--accent);
font-family: var(--mono);
font-size: 1rem;
color: var(--accent);
margin-top: 1.5rem;
flex-shrink: 0;
transition: background 0.1s, color 0.1s;
}
.project-card:hover .project-card__arrow {
background: var(--accent);
color: var(--black);
}
/* — Tags — */
@@ -53,7 +141,7 @@
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
margin-top: 1rem;
margin-top: 0.5rem;
}
.tag {
@@ -68,6 +156,76 @@
color: var(--grey-text);
}
/* — Case study breadcrumb bar — */
.cs-breadcrumb {
display: flex;
justify-content: space-between;
align-items: center;
background: var(--black);
border: 4px solid var(--black);
box-shadow: 8px 8px 0 var(--black);
padding: 0.75rem 1.25rem;
margin-bottom: 2rem;
}
.cs-breadcrumb-label {
font-family: var(--mono);
font-size: 0.75rem;
font-weight: 700;
color: var(--accent);
letter-spacing: 0.04em;
}
/* — Case study inline meta bar — */
.cs-meta-bar {
list-style: none;
display: flex;
flex-wrap: wrap;
gap: 0 2rem;
padding: 0.75rem 1rem;
background: var(--grey-dark);
border-left: 6px solid var(--grey-text);
/*border-top: 4px solid var(--grey-text);*/
margin-top: 1.5rem;
max-width: clamp(380px, 55vw, 750px);
box-shadow: var(--shadow);
}
.cs-meta-bar li {
font-family: var(--mono);
font-size: 0.8rem;
font-weight: 400;
color: var(--grey-text);
line-height: 1.8;
}
/* — Case study back link — */
.cs-back {
display: inline-block;
font-family: var(--mono);
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--accent);
text-decoration: none;
margin-bottom: 1.5rem;
transition: opacity 0.1s;
}
.cs-back:hover {
opacity: 0.7;
}
.cs-back--lg {
font-size: 0.85rem;
display: block;
margin-bottom: 2rem;
}
/* — Case study meta grid — */
.cs-meta-grid {
@@ -165,6 +323,11 @@
}
@media (max-width: 768px) {
.portfolio-grid {
grid-template-columns: repeat(2, 1fr);
gap: 1.25rem;
}
.screenshot-hero-wrap {
width: 100%;
}
@@ -173,3 +336,10 @@
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.portfolio-grid {
grid-template-columns: 1fr;
gap: 1rem;
}
}

View File

@@ -97,11 +97,11 @@ a {
======================================== */
.hero {
min-height: 100vh;
min-height: 60vh;
display: flex;
flex-direction: column;
justify-content: center;
padding: 10rem 3rem 6rem;
padding: 4rem 3rem 2rem;
border: var(--border-thick);
background: var(--grey-light);
margin-bottom: 2rem;
@@ -137,8 +137,8 @@ a {
color: var(--white);
background: var(--black);
padding: 1rem;
max-width: 550px;
margin-bottom: 3rem;
max-width: clamp(280px, 40vw, 550px);
margin-bottom: 1rem;
border-left: 8px solid var(--accent);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

BIN
img/thompson/hero.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 413 KiB

View File

@@ -21,7 +21,7 @@
<div class="nav-links">
<a href="/#about">About</a>
<a href="/#services">Services</a>
<a href="/portfolio"<?php echo ($current === '/portfolio' || $current === '/portfolio/') ? ' class="active"' : ''; ?>>Work</a>
<a href="/portfolio"<?php echo (str_starts_with($current, '/portfolio')) ? ' class="active"' : ''; ?>>Work</a>
<a href="/#contact">Contact</a>
</div>
</nav>

1232
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "uovidiu",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://gitea.local.uovidiu.com/bitstream/uovidiu.com.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"devDependencies": {
"puppeteer": "^24.39.0"
}
}

View File

@@ -3,6 +3,159 @@ $title = 'Portfolio — Ovidiu Ungureanu';
$description = 'Selected web development projects by Ovidiu Ungureanu.';
$extra_css = '/css/portfolio.css';
require __DIR__ . '/../includes/header.php';
$projects = [
[
'slug' => 'thompson',
'num' => '01',
'code' => 'WEB_DEV',
'title' => 'Thompson Service Centre',
'desc' => 'Trust-first redesign for a 25-year Peterborough garage. Bold credibility signals, booking flow, zero clichés.',
'year' => '2025',
'thumb' => '/img/thompson/hero.jpg',
'live' => true,
],
[
'slug' => 'northgate-motors',
'num' => '02',
'code' => 'E_COMMERCE',
'title' => 'Northgate Motors',
'desc' => 'Parts and accessories e-commerce with Stripe checkout and stock management backend.',
'year' => '2025',
'thumb' => null,
'live' => false,
],
[
'slug' => 'fenland-tyres',
'num' => '03',
'code' => 'BOOKING',
'title' => 'Fenland Tyres',
'desc' => 'Online booking system with real-time slot availability and automated confirmation emails.',
'year' => '2025',
'thumb' => null,
'live' => false,
],
[
'slug' => 'stamford-autocare',
'num' => '04',
'code' => 'LANDING',
'title' => 'Stamford Autocare',
'desc' => 'High-conversion landing page focused on local SEO and a single clear call-to-action.',
'year' => '2024',
'thumb' => null,
'live' => false,
],
[
'slug' => 'heron-storage',
'num' => '05',
'code' => 'WEB_DEV',
'title' => 'Heron Self Storage',
'desc' => 'Full site rebuild with unit availability checker and online reservation management.',
'year' => '2024',
'thumb' => null,
'live' => false,
],
[
'slug' => 'ely-clutch',
'num' => '06',
'code' => 'LANDING',
'title' => 'Ely Clutch & Brake',
'desc' => 'Fast-loading landing page emphasising specialist credentials and emergency call-out service.',
'year' => '2024',
'thumb' => null,
'live' => false,
],
[
'slug' => 'wisbech-auto',
'num' => '07',
'code' => 'FULL_BUILD',
'title' => 'Wisbech Auto Centre',
'desc' => 'Full-stack build: service menu, booking, customer portal, and GSAP scroll animations.',
'year' => '2024',
'thumb' => null,
'live' => false,
],
[
'slug' => 'march-mot',
'num' => '08',
'code' => 'BOOKING',
'title' => 'March MOT Station',
'desc' => 'Streamlined MOT booking with DVLA vehicle lookup and reminder SMS integration.',
'year' => '2024',
'thumb' => null,
'live' => false,
],
[
'slug' => 'peterborough-panels',
'num' => '09',
'code' => 'PORTFOLIO',
'title' => 'Peterborough Panels',
'desc' => 'Before/after portfolio site for a bodywork specialist. Gallery-first layout, zero clutter.',
'year' => '2023',
'thumb' => null,
'live' => false,
],
[
'slug' => 'huntingdon-haulage',
'num' => '10',
'code' => 'WEB_DEV',
'title' => 'Huntingdon Haulage',
'desc' => 'Fleet services website with route coverage map and driver recruitment section.',
'year' => '2023',
'thumb' => null,
'live' => false,
],
[
'slug' => 'chatteris-cars',
'num' => '11',
'code' => 'LISTINGS',
'title' => 'Chatteris Cars',
'desc' => 'Used car listings with dynamic search, filter by make/model, and finance enquiry form.',
'year' => '2023',
'thumb' => null,
'live' => false,
],
[
'slug' => 'ramsey-recovery',
'num' => '12',
'code' => 'LANDING',
'title' => 'Ramsey Recovery',
'desc' => '24/7 roadside recovery landing page built for speed — sub-2s load, click-to-call hero.',
'year' => '2023',
'thumb' => null,
'live' => false,
],
[
'slug' => 'soham-services',
'num' => '13',
'code' => 'FULL_BUILD',
'title' => 'Soham Service Station',
'desc' => 'Complete digital overhaul: site, booking system, staff rota tool, and customer history log.',
'year' => '2023',
'thumb' => null,
'live' => false,
],
[
'slug' => 'downham-diesel',
'num' => '14',
'code' => 'LANDING',
'title' => 'Downham Diesel',
'desc' => 'Specialist diesel landing page targeting commercial vehicle operators in the Fens.',
'year' => '2022',
'thumb' => null,
'live' => false,
],
[
'slug' => 'kings-lynn-kustoms',
'num' => '15',
'code' => 'PORTFOLIO',
'title' => "King's Lynn Kustoms",
'desc' => 'Custom builds portfolio for a one-man fabrication shop. Dramatic full-screen imagery.',
'year' => '2022',
'thumb' => null,
'live' => false,
],
];
?>
<header class="hero">
@@ -11,135 +164,38 @@ require __DIR__ . '/../includes/header.php';
<p class="hero-sub">Client projects and builds.</p>
</header>
<!-- PROJECT INDEX -->
<section class="section">
<div class="section-label">001 — Projects</div>
<?php
$projects = [
[
'num' => '01',
'title' => 'Thompson Service Centre',
'type' => 'Website Redesign',
'year' => '2025',
'tags' => ['HTML', 'Tailwind CSS v4', 'GSAP'],
'anchor' => 'thompson',
],
];
foreach ($projects as $p): ?>
<a href="#<?php echo $p['anchor']; ?>" class="project-card-link">
<div class="service-item project-card-inner">
<span class="service-num"><?php echo $p['num']; ?></span>
<div class="project-card-head">
<h3><?php echo htmlspecialchars($p['title']); ?></h3>
<span class="project-card-year"><?php echo htmlspecialchars($p['year']); ?></span>
<div class="section-label">001 — <?php echo count($projects); ?> Projects</div>
<div class="portfolio-grid">
<?php foreach ($projects as $p): ?>
<a href="/portfolio/<?php echo htmlspecialchars($p['slug']); ?>/"
class="project-card<?php echo $p['live'] ? '' : ' project-card--wip'; ?>">
<div class="project-card__thumb">
<span class="project-card__label">[<?php echo htmlspecialchars($p['num']); ?> / <?php echo htmlspecialchars($p['code']); ?>]</span>
<?php if ($p['thumb']): ?>
<img src="<?php echo htmlspecialchars($p['thumb']); ?>"
alt="<?php echo htmlspecialchars($p['title']); ?>"
loading="lazy">
<?php endif; ?>
</div>
<p><?php echo htmlspecialchars($p['type']); ?></p>
<div class="tags-row">
<?php foreach ($p['tags'] as $tag): ?>
<span class="tag"><?php echo htmlspecialchars($tag); ?></span>
<?php endforeach; ?>
<div class="project-card__body">
<h3 class="project-card__title"><?php echo htmlspecialchars($p['title']); ?></h3>
<p class="project-card__desc"><?php echo htmlspecialchars($p['desc']); ?></p>
<div class="project-card__footer">
<span class="project-card__year"><?php echo htmlspecialchars($p['year']); ?></span>
<span class="project-card__arrow">→</span>
</div>
<span class="project-card-cta">View Case Study →</span>
</div>
</a>
<?php endforeach; ?>
</section>
<!-- CASE STUDY: THOMPSON SERVICE CENTRE -->
<section id="thompson" class="section">
<div class="section-label">Case Study — 01</div>
<p class="large-text">Thompson<br>Service Centre</p>
<div class="services-grid cs-meta-grid">
<div class="service-item">
<span class="service-num">Type</span>
<h3>Website Redesign</h3>
</div>
<div class="service-item">
<span class="service-num">Stack</span>
<h3>HTML · Tailwind CSS v4 · GSAP</h3>
</div>
<div class="service-item">
<span class="service-num">Client</span>
<h3>Thompson Service Centre, Peterborough</h3>
</div>
<div class="service-item">
<span class="service-num">Year</span>
<h3>2025</h3>
</div>
</div>
</section>
<section class="section">
<div class="section-label">Project Overview</div>
<div class="section-content">
<p>Thompson Service Centre needed more than a website — they needed a digital presence that matched 25 years of earned trust. We built a high-fidelity landing page that puts credibility front and centre: a bold hero section, a trust bar loaded with real credentials, and service cards that communicate competence without the usual garage-site clichés. Every element was designed to make a first-time visitor feel like they'd already found their mechanic.</p>
</div>
</section>
<section class="section">
<div class="section-label">Homepage</div>
<div class="screenshot-hero-wrap">
<div class="browser-chrome">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<span class="browser-url">thompsonservicecentre.co.uk</span>
</div>
<img
src="/img/thompson/hero.png"
alt="Thompson Service Centre — Homepage"
class="screenshot-hero"
loading="lazy"
>
</div>
</section>
<section class="section">
<div class="section-label">Booking Flow</div>
<div class="services-grid">
<div class="service-item screenshot-card">
<span class="service-num">Vehicle Lookup</span>
<div class="browser-chrome browser-chrome--sm">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<span class="browser-url">thompsonservicecentre.co.uk/book</span>
</div>
<img
src="/img/thompson/booking-lookup.png"
alt="Vehicle registration lookup"
class="screenshot-screen"
loading="lazy"
>
</div>
<div class="service-item screenshot-card">
<span class="service-num">Booking Form</span>
<div class="browser-chrome browser-chrome--sm">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<span class="browser-url">thompsonservicecentre.co.uk/book</span>
</div>
<img
src="/img/thompson/booking-confirm.png"
alt="Vehicle confirmed and booking form"
class="screenshot-screen"
loading="lazy"
>
</div>
</div>
</section>
<section class="section">
<div class="section-label">Craft & Execution</div>
<div class="section-content">
<p>Built with vanilla HTML, Tailwind CSS v4, and GSAP-powered scroll animations, the page loads fast and feels premium on every device. The design system — rooted in Workshop Navy and Safety Orange — draws from real automotive heritage rather than off-the-shelf templates. Noise overlays, layered gradients, and spring-eased micro-interactions give it a tactile warmth that sets it apart from every other garage site in the postcode.</p>
</div>
</section>
<section class="section section-cta">
<div class="section-label">005 — Contact</div>
<div class="section-label">002 — Contact</div>
<div class="section-content">
<p class="large-text">Got a project in mind?</p>
<a href="mailto:hello@uovidiu.com" class="btn">Get in Touch →</a>

View File

@@ -0,0 +1,112 @@
<?php
/*
* CASE STUDY PAGE TEMPLATE
* ─────────────────────────────────────────────────────────────────
* Each project gets its own directory under portfolio/{slug}/
* Copy this file as a starting point for new case studies.
*
* Required: $title, $description, $extra_css before require header.
* Back link: always points to /portfolio/ via .cs-back anchor.
* Images: store in /img/{slug}/ — see naming convention in CLAUDE.md.
* ─────────────────────────────────────────────────────────────────
*/
$title = 'Thompson Service Centre — Case Study';
$description = 'How we built a trust-first digital presence for a 25-year Peterborough garage.';
$extra_css = '/css/portfolio.css';
require __DIR__ . '/../../includes/header.php';
?>
<div class="cs-breadcrumb">
<a href="/portfolio/" class="cs-back">← All Work</a>
<span class="cs-breadcrumb-label">[01 / WEB_DEV]</span>
</div>
<header class="hero">
<div class="hero-label">Case Study — 01</div>
<h1>Thompson<br>Service Centre</h1>
<p class="hero-sub">Website Redesign · 2025</p>
<ul class="cs-meta-bar">
<li>HTML · Tailwind CSS v4 · GSAP</li>
<li>Thompson Service Centre, Peterborough</li>
<li>2025</li>
</ul>
</header>
<section class="section">
<div class="section-label">Project Overview</div>
<div class="section-content">
<p>Thompson Service Centre needed more than a website — they needed a digital presence that matched 25 years of earned trust. We built a high-fidelity landing page that puts credibility front and centre: a bold hero section, a trust bar loaded with real credentials, and service cards that communicate competence without the usual garage-site clichés. Every element was designed to make a first-time visitor feel like they'd already found their mechanic.</p>
</div>
</section>
<section class="section">
<div class="section-label">Homepage</div>
<div class="screenshot-hero-wrap">
<div class="browser-chrome">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<span class="browser-url">thompsonservicecentre.co.uk</span>
</div>
<img
src="/img/thompson/hero.jpg"
alt="Thompson Service Centre — Homepage"
class="screenshot-hero"
loading="lazy"
>
</div>
</section>
<section class="section">
<div class="section-label">Booking Flow</div>
<div class="services-grid">
<div class="service-item screenshot-card">
<span class="service-num">Vehicle Lookup</span>
<div class="browser-chrome browser-chrome--sm">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<span class="browser-url">thompsonservicecentre.co.uk/book</span>
</div>
<img
src="/img/thompson/booking-lookup.jpg"
alt="Vehicle registration lookup"
class="screenshot-screen"
loading="lazy"
>
</div>
<div class="service-item screenshot-card">
<span class="service-num">Booking Form</span>
<div class="browser-chrome browser-chrome--sm">
<div class="browser-dots">
<span></span><span></span><span></span>
</div>
<span class="browser-url">thompsonservicecentre.co.uk/book</span>
</div>
<img
src="/img/thompson/booking-confirm.jpg"
alt="Vehicle confirmed and booking form"
class="screenshot-screen"
loading="lazy"
>
</div>
</div>
</section>
<section class="section">
<div class="section-label">Craft & Execution</div>
<div class="section-content">
<p>Built with vanilla HTML, Tailwind CSS v4, and GSAP-powered scroll animations, the page loads fast and feels premium on every device. The design system — rooted in Workshop Navy and Safety Orange — draws from real automotive heritage rather than off-the-shelf templates. Noise overlays, layered gradients, and spring-eased micro-interactions give it a tactile warmth that sets it apart from every other garage site in the postcode.</p>
</div>
</section>
<section class="section section-cta">
<div class="section-label">— Next</div>
<div class="section-content">
<a href="/portfolio/" class="cs-back cs-back--lg">← Back to All Work</a>
<p class="large-text">Got a project in mind?</p>
<a href="mailto:hello@uovidiu.com" class="btn">Get in Touch →</a>
</div>
</section>
<?php require __DIR__ . '/../../includes/footer.php'; ?>

View File

@@ -4,7 +4,7 @@ import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const url = process.argv[2] || 'http://localhost:3000';
const url = process.argv[2] || 'http://uovidiu.test';
const label = process.argv[3] || '';
const dir = path.join(__dirname, 'temporary screenshots');