Remake portfolio landing page
Lint / lint (push) Successful in 26s

This commit is contained in:
Julien Valverdé
2026-07-27 06:27:24 +02:00
parent 40bba2cc23
commit cf8bef6abd
11 changed files with 1204 additions and 168 deletions
-13
View File
@@ -61,7 +61,6 @@
"react-dom": "^19.2.6",
"react-i18next": "^17.0.8",
"react-icons": "^5.6.0",
"react-shadow": "^20.6.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0",
},
@@ -714,8 +713,6 @@
"html-parse-stringify": ["html-parse-stringify@3.0.1", "", { "dependencies": { "void-elements": "3.1.0" } }, "sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg=="],
"humps": ["humps@2.0.1", "", {}, "sha512-E0eIbrFWUhwfXJmsbdjRQFQPrl5pTEoKlz163j1mTqqUnU9PgR4AgB8AIITzuB3vLBdxZXyZ9TDIrwB2OASz4g=="],
"i18next": ["i18next@26.2.0", "", { "peerDependencies": { "typescript": "^5 || ^6" }, "optionalPeers": ["typescript"] }, "sha512-zwBHldHdTmwN7r6UNc7lC6GWNN+YYg3DrRSeHR5PRRBf5QnJZcYHrQc0uaU26qZeYxR7iFZD+Y315dPnKP47wA=="],
"i18next-browser-languagedetector": ["i18next-browser-languagedetector@8.2.1", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-bZg8+4bdmaOiApD7N7BPT9W8MLZG+nPTOFlLiJiT8uzKXFjhxw4v2ierCXOwB5sFDMtuA5G4kgYZ0AznZxQ/cw=="],
@@ -790,8 +787,6 @@
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw=="],
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": { "loose-envify": "cli.js" } }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@1.16.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-dYwyPzb4MEKpGUmNYk3WKWPnMrHs3FKM+q94kAnJrcDIqqn1hq2xY8scaS2ovsOCM5D51ey2gaRG3PBb1vgoYQ=="],
@@ -824,8 +819,6 @@
"npm-sort": ["npm-sort@0.0.4", "", { "bin": { "npm-sort": "./index.js" } }, "sha512-S5Id/3Jvr7Cf/QnWjRteprngERCBhhEFOM+wMhUrAYP060/HUBC1aL5GoXS3xITlgacJCWaSmP4HQaAt91nNYQ=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g=="],
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="],
@@ -850,8 +843,6 @@
"prettier": ["prettier@3.8.3", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw=="],
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
"punycode": ["punycode@2.3.1", "", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
"pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
@@ -864,10 +855,6 @@
"react-icons": ["react-icons@5.6.0", "", { "peerDependencies": { "react": "*" } }, "sha512-RH93p5ki6LfOiIt0UtDyNg/cee+HLVR6cHHtW3wALfo+eOHTp8RnU2kRkI6E+H19zMIs03DyxUG/GfZMOGvmiA=="],
"react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
"react-shadow": ["react-shadow@20.6.0", "", { "dependencies": { "humps": "^2.0.1" }, "peerDependencies": { "prop-types": "^15.0.0", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-kY+w4OMNZ8Nj9YI9eiTgvvJ/wYO7XyX1D/LYhvwQZv5vw69iCiDtGB0BX/2U8gLUuZAMN+x/7rHJKqHh8wXFHQ=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
+11 -2
View File
@@ -2,9 +2,18 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
<meta name="theme-color" content="#e9e9e4" />
<meta
name="description"
content="Julien Valverde — full-stack engineer and product builder working on Effect, developer tools, and useful vertical software."
/>
<meta property="og:title" content="Julien Valverde — Full-stack engineer & product builder" />
<meta
property="og:description"
content="Open-source tools, production software, and systems currently taking shape."
/>
<title>Julien Valverde — Full-stack engineer &amp; product builder</title>
</head>
<body>
<div id="root"></div>
-1
View File
@@ -29,7 +29,6 @@
"react-dom": "^19.2.6",
"react-i18next": "^17.0.8",
"react-icons": "^5.6.0",
"react-shadow": "^20.6.0",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.0"
},
@@ -0,0 +1,52 @@
import { ArrowUpRight } from "lucide-react"
interface ProjectCardProps {
readonly index: string
readonly title: string
readonly category: string
readonly description: string
readonly tags: ReadonlyArray<string>
readonly href?: string
readonly linkLabel?: string
readonly variant?: "light" | "dark" | "outline"
}
export function ProjectCard({
index,
title,
category,
description,
tags,
href,
linkLabel = "View project",
variant = "light",
}: ProjectCardProps) {
return (
<article className={`project-card project-card--${variant}`}>
<div className="project-card__topline">
<span>{index}</span>
<span>{category}</span>
</div>
<div className="project-card__body">
<h3>{title}</h3>
<p>{description}</p>
</div>
<div className="project-card__footer">
<ul className="tag-list" aria-label={`${title} technologies and themes`}>
{tags.map(tag => <li key={tag}>{tag}</li>)}
</ul>
{href
? (
<a className="project-card__link" href={href} target="_blank" rel="noreferrer">
{linkLabel}
<ArrowUpRight aria-hidden="true" />
</a>
)
: <span className="project-card__status">In development <span aria-hidden="true"></span></span>}
</div>
</article>
)
}
@@ -0,0 +1,17 @@
interface SectionHeadingProps {
readonly eyebrow: string
readonly title: string
readonly description?: string
}
export function SectionHeading({ eyebrow, title, description }: SectionHeadingProps) {
return (
<header className="section-heading">
<p className="section-heading__eyebrow">{eyebrow}</p>
<div className="section-heading__body">
<h2>{title}</h2>
{description && <p>{description}</p>}
</div>
</header>
)
}
@@ -0,0 +1,34 @@
export function Win98Status() {
return (
<div className="win98-host">
<div className="win98-window">
<div className="win98-title-bar">
<div className="win98-title-bar__text">julien.exe System status</div>
<div className="win98-title-bar__controls" aria-hidden="true">
<span className="win98-control win98-control--minimize" />
<span className="win98-control win98-control--maximize" />
<span className="win98-control win98-control--close" />
</div>
</div>
<div className="win98-body">
<div className="win98-copy">
<strong>Building software that earns its place.</strong>
Full-stack product engineering with a taste for typed systems, sharp interfaces, and useful tools.
</div>
<ul className="win98-status-list">
<li><span>Location</span><span>Chambéry, FR</span></li>
<li><span>Current mode</span><span>Shipping</span></li>
<li><span>Signal</span><span>Available</span></li>
</ul>
<div className="win98-status-bar">
<p>Effect / TypeScript</p>
<p>5 projects loaded</p>
</div>
</div>
</div>
</div>
)
}
+911 -47
View File
@@ -1,58 +1,922 @@
/** biome-ignore-all lint/suspicious/noUnknownAtRules: Tailwind */
/** biome-ignore-all lint/style/noDescendingSpecificity: Component selectors intentionally share a single stylesheet */
@import "tailwindcss";
@import "tw-animate-css";
@import "@fontsource/work-sans";
@import "@fontsource/work-sans/400.css";
@import "@fontsource/work-sans/500.css";
@import "@fontsource/work-sans/600.css";
@import "@fontsource/work-sans/700.css";
@import "@fontsource/work-sans/800.css";
@font-face {
font-family: "Pixelated MS Sans Serif";
src: url("/98.css/fonts/converted/ms_sans_serif.woff2") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 400;
}
@font-face {
font-family: "Pixelated MS Sans Serif";
src: url("/98.css/fonts/converted/ms_sans_serif_bold.woff2") format("woff2");
font-display: swap;
font-style: normal;
font-weight: 700;
}
:root {
--background: hsl(220 23% 95%);
--secondary-background: oklch(100% 0 0);
--foreground: oklch(0% 0 0);
--main-foreground: oklch(0% 0 0);
--main: hsl(220 23% 95%);
--border: oklch(0% 0 0);
--ring: oklch(0% 0 0);
--overlay: oklch(0% 0 0 / 0.8);
--shadow: 4px 4px 0px 0px var(--border);
--chart-1: #FF7A05;
--chart-2: #0099FF;
--chart-3: #FFBF00;
--chart-4: #00D696;
--chart-5: #7A83FF;
--chart-active-dot: #000;
--paper: #e9e9e4;
--ink: #0b0b0b;
--white: #fff;
--gray: #c9c9c4;
--line: 2px solid var(--ink);
--shadow: 7px 7px 0 var(--ink);
color: var(--ink);
background: var(--paper);
font-family: "Work Sans", Arial, sans-serif;
font-synthesis: none;
text-rendering: optimizeLegibility;
}
@theme inline {
--color-main: var(--main);
--color-background: var(--background);
--color-secondary-background: var(--secondary-background);
--color-foreground: var(--foreground);
--color-main-foreground: var(--main-foreground);
--color-border: var(--border);
--color-overlay: var(--overlay);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--spacing-boxShadowX: 4px;
--spacing-boxShadowY: 4px;
--spacing-reverseBoxShadowX: -4px;
--spacing-reverseBoxShadowY: -4px;
--radius-base: 5px;
--shadow-shadow: var(--shadow);
--font-weight-base: 500;
--font-weight-heading: 700;
* {
box-sizing: border-box;
}
@layer base {
body {
@apply text-foreground font-base bg-background;
font-family: "Work Sans", sans-serif;
}
html {
scroll-behavior: smooth;
}
h1, h2, h3, h4, h5, h6{
@apply font-heading;
body {
min-width: 320px;
margin: 0;
overflow-x: clip;
background:
linear-gradient(90deg, rgb(0 0 0 / 4%) 1px, transparent 1px),
linear-gradient(rgb(0 0 0 / 4%) 1px, transparent 1px),
var(--paper);
background-size: 32px 32px;
}
body,
a,
button {
cursor: default;
}
a {
color: inherit;
text-decoration: none;
}
a:hover,
button:hover {
cursor: pointer;
}
::selection {
color: var(--white);
background: var(--ink);
}
.site-header {
position: relative;
z-index: 5;
display: flex;
align-items: center;
justify-content: space-between;
min-height: 88px;
padding: 16px clamp(20px, 4vw, 64px);
border-bottom: var(--line);
background: rgb(233 233 228 / 92%);
backdrop-filter: blur(8px);
}
.site-mark {
display: flex;
align-items: center;
gap: 12px;
font-size: 11px;
font-weight: 700;
line-height: 1.15;
text-transform: uppercase;
}
.site-mark > span:first-child {
display: grid;
width: 46px;
height: 46px;
place-items: center;
border: var(--line);
background: var(--ink);
color: var(--white);
font-size: 18px;
letter-spacing: -1px;
}
.site-header nav {
display: flex;
align-items: center;
gap: clamp(20px, 3vw, 48px);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
}
.site-header nav a {
border-bottom: 2px solid transparent;
}
.site-header nav a:hover,
.site-header nav a:focus-visible {
border-color: var(--ink);
}
.hero {
display: grid;
grid-template-columns: minmax(0, 1.55fr) minmax(320px, 0.65fr);
min-height: calc(100svh - 88px);
border-bottom: var(--line);
}
.hero__intro {
display: flex;
flex-direction: column;
justify-content: space-between;
min-width: 0;
padding: clamp(42px, 7vw, 104px) clamp(20px, 4vw, 64px) 48px;
border-right: var(--line);
}
.hero__kicker,
.section-heading__eyebrow,
.repo-section__eyebrow {
margin: 0;
font-size: 12px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.hero h1 {
max-width: 1000px;
margin: 44px 0;
font-size: clamp(68px, 10.5vw, 180px);
font-weight: 800;
letter-spacing: -0.085em;
line-height: 0.76;
text-transform: uppercase;
}
.hero h1 span {
display: block;
color: var(--paper);
-webkit-text-stroke: clamp(1.5px, 0.16vw, 3px) var(--ink);
text-indent: 0.22em;
}
.hero__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 32px;
}
.hero__footer > p {
max-width: 570px;
margin: 0;
overflow-wrap: anywhere;
font-size: clamp(17px, 1.5vw, 22px);
font-weight: 500;
line-height: 1.35;
}
.brutal-button {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
justify-content: center;
gap: 14px;
min-height: 54px;
padding: 0 20px;
border: var(--line);
background: var(--white);
box-shadow: var(--shadow);
font-size: 12px;
font-weight: 700;
text-transform: uppercase;
transition: transform 120ms ease, box-shadow 120ms ease;
}
.brutal-button svg {
width: 17px;
}
.brutal-button:hover,
.brutal-button:focus-visible {
box-shadow: 2px 2px 0 var(--ink);
transform: translate(5px, 5px);
}
.hero__status {
position: relative;
display: flex;
align-items: center;
padding: clamp(32px, 5vw, 72px);
overflow: hidden;
background:
radial-gradient(circle, var(--ink) 1px, transparent 1.5px) 0 0 / 18px 18px,
var(--gray);
}
.hero__status::before {
position: absolute;
width: 230px;
height: 230px;
border: 3px solid var(--ink);
border-radius: 50%;
content: "";
inset: -90px -80px auto auto;
}
.hero__stamp {
position: absolute;
right: -39px;
bottom: 82px;
padding: 7px 16px;
border: var(--line);
background: var(--paper);
font-size: 10px;
font-weight: 700;
letter-spacing: 0.16em;
transform: rotate(-90deg);
}
.win98-host {
position: relative;
z-index: 1;
width: min(100%, 430px);
margin: auto;
filter: drop-shadow(10px 10px 0 rgb(0 0 0 / 80%));
transform: rotate(1.5deg);
transition: transform 180ms ease;
}
.win98-host:hover {
transform: rotate(0);
}
.win98-window {
width: 100%;
padding: 3px;
background: #c0c0c0;
box-shadow:
inset -1px -1px #0a0a0a,
inset 1px 1px #dfdfdf,
inset -2px -2px #808080,
inset 2px 2px #fff;
color: #111;
font-family: "Pixelated MS Sans Serif", Arial, sans-serif;
-webkit-font-smoothing: none;
font-size: 11px;
}
.win98-title-bar {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 20px;
padding: 3px 2px 3px 3px;
background: linear-gradient(90deg, #111, #666);
}
.win98-title-bar__text {
overflow: hidden;
color: #fff;
font-weight: 700;
text-overflow: ellipsis;
white-space: nowrap;
}
.win98-title-bar__controls {
display: flex;
gap: 2px;
}
.win98-control {
display: block;
width: 16px;
height: 14px;
background-color: #c0c0c0;
background-position: center;
background-repeat: no-repeat;
box-shadow:
inset -1px -1px #0a0a0a,
inset 1px 1px #fff,
inset -2px -2px #808080,
inset 2px 2px #dfdfdf;
}
.win98-control--minimize {
background-image: url("/98.css/icon/minimize.svg");
}
.win98-control--maximize {
background-image: url("/98.css/icon/maximize.svg");
}
.win98-control--close {
background-image: url("/98.css/icon/close.svg");
}
.win98-body {
margin: 16px 12px 12px;
}
.win98-copy {
font-size: 13px;
line-height: 1.5;
}
.win98-copy strong {
display: block;
margin-bottom: 8px;
font-size: 14px;
}
.win98-status-list {
display: grid;
gap: 8px;
margin: 16px 0;
padding: 0;
list-style: none;
}
.win98-status-list li {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
}
.win98-status-list span:last-child {
font-weight: 700;
}
.win98-status-bar {
display: flex;
gap: 2px;
}
.win98-status-bar p {
flex: 1;
margin: 0;
padding: 3px 5px;
box-shadow: inset -1px -1px #dfdfdf, inset 1px 1px #808080;
}
.ticker {
overflow: hidden;
border-bottom: var(--line);
background: var(--ink);
color: var(--white);
}
.ticker > div {
display: flex;
width: max-content;
align-items: center;
gap: 26px;
padding: 14px 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
animation: marquee 24s linear infinite;
}
.ticker i {
font-style: normal;
}
@keyframes marquee {
to {
transform: translateX(-50%);
}
}
.work-section,
.upcoming-section {
padding: clamp(76px, 10vw, 150px) clamp(20px, 4vw, 64px);
border-bottom: var(--line);
}
.section-heading {
display: grid;
grid-template-columns: minmax(160px, 0.38fr) minmax(0, 1fr);
gap: clamp(28px, 5vw, 90px);
max-width: 1440px;
margin: 0 auto clamp(48px, 7vw, 100px);
}
.section-heading__eyebrow {
padding-top: 14px;
border-top: var(--line);
}
.section-heading__body {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(220px, 0.42fr);
gap: 40px;
align-items: end;
}
.section-heading h2,
.repo-section h2 {
margin: 0;
font-size: clamp(46px, 6.2vw, 100px);
font-weight: 800;
letter-spacing: -0.065em;
line-height: 0.9;
text-transform: uppercase;
}
.section-heading__body > p {
max-width: 420px;
margin: 0 0 5px;
font-size: 17px;
line-height: 1.45;
}
.project-grid,
.upcoming-grid {
display: grid;
grid-template-columns: repeat(12, 1fr);
gap: clamp(22px, 3vw, 44px);
max-width: 1440px;
margin: 0 auto;
}
.project-grid .project-card:first-child {
grid-column: span 7;
}
.project-grid .project-card:nth-child(2) {
grid-column: span 5;
margin-top: 90px;
}
.project-grid .project-card:nth-child(3) {
grid-column: 3 / span 8;
}
.upcoming-grid .project-card {
grid-column: span 6;
}
.project-card {
position: relative;
display: flex;
min-height: 480px;
flex-direction: column;
justify-content: space-between;
padding: clamp(24px, 3.5vw, 52px);
border: var(--line);
background: var(--white);
box-shadow: var(--shadow);
transition: transform 160ms ease, box-shadow 160ms ease;
}
.project-card:hover {
box-shadow: 11px 11px 0 var(--ink);
transform: translate(-4px, -4px);
}
.project-card--dark {
background: var(--ink);
box-shadow: 7px 7px 0 #777;
color: var(--white);
}
.project-card--dark:hover {
box-shadow: 11px 11px 0 #777;
}
.project-card--outline {
background:
repeating-linear-gradient(-45deg, transparent 0 12px, rgb(0 0 0 / 4%) 12px 24px),
var(--paper);
}
.project-card__topline {
display: flex;
justify-content: space-between;
gap: 24px;
padding-bottom: 15px;
border-bottom: 1px solid currentColor;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.project-card__body {
margin: 70px 0;
}
.project-card h3 {
margin: 0 0 28px;
font-size: clamp(46px, 6vw, 92px);
font-weight: 800;
letter-spacing: -0.075em;
line-height: 0.86;
}
.project-card__body p {
max-width: 680px;
margin: 0;
font-size: clamp(16px, 1.35vw, 20px);
line-height: 1.5;
}
.project-card__footer {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 30px;
}
.tag-list {
display: flex;
flex-wrap: wrap;
gap: 7px;
margin: 0;
padding: 0;
list-style: none;
}
.tag-list li {
padding: 6px 9px;
border: 1px solid currentColor;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
}
.project-card__link,
.project-card__status {
display: inline-flex;
flex: 0 0 auto;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.project-card__link {
padding-bottom: 4px;
border-bottom: 2px solid currentColor;
}
.project-card__link svg {
width: 15px;
}
.project-card__link:hover svg {
transform: translate(2px, -2px);
}
.project-card__status span {
animation: blink 1.5s steps(1) infinite;
}
@keyframes blink {
50% {
opacity: 0.2;
}
}
.upcoming-section {
background: var(--gray);
}
.about-section {
display: grid;
grid-template-columns: minmax(180px, 0.35fr) minmax(0, 1fr);
min-height: 720px;
border-bottom: var(--line);
}
.about-section__label {
display: flex;
flex-direction: column;
justify-content: space-between;
padding: clamp(32px, 5vw, 72px);
border-right: var(--line);
background: var(--ink);
color: var(--white);
font-size: 13px;
font-weight: 700;
text-transform: uppercase;
}
.about-section__label span:first-child {
font-size: clamp(76px, 11vw, 180px);
letter-spacing: -0.09em;
line-height: 0.8;
}
.about-section__copy {
display: flex;
flex-direction: column;
justify-content: center;
gap: clamp(60px, 8vw, 120px);
padding: clamp(58px, 9vw, 140px) clamp(24px, 7vw, 110px);
}
.about-section__lead {
max-width: 1100px;
margin: 0;
font-size: clamp(38px, 5.2vw, 82px);
font-weight: 700;
letter-spacing: -0.055em;
line-height: 1;
}
.about-section__columns {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: clamp(32px, 6vw, 100px);
max-width: 950px;
font-size: 17px;
line-height: 1.55;
}
.about-section__columns p {
margin: 0;
padding-top: 16px;
border-top: var(--line);
}
.repo-section {
padding: clamp(76px, 10vw, 150px) clamp(20px, 4vw, 64px);
border-bottom: var(--line);
background: var(--white);
}
.repo-section__eyebrow {
margin-bottom: 32px;
}
.repo-section h2 {
margin-bottom: clamp(50px, 7vw, 100px);
}
.repo-links {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 28px;
}
.repo-links > a {
display: grid;
grid-template-columns: auto 1fr auto;
gap: 20px;
align-items: center;
min-width: 0;
padding: clamp(22px, 3vw, 38px);
border: var(--line);
box-shadow: 5px 5px 0 var(--ink);
font-size: clamp(16px, 2vw, 25px);
font-weight: 700;
transition: transform 120ms ease, box-shadow 120ms ease;
}
.repo-links > a:hover,
.repo-links > a:focus-visible {
box-shadow: 1px 1px 0 var(--ink);
transform: translate(4px, 4px);
}
.repo-links svg {
width: 25px;
height: 25px;
}
.repo-links span {
overflow: hidden;
text-overflow: ellipsis;
}
.repo-links small {
display: block;
margin-bottom: 4px;
font-size: 10px;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
}
.site-footer {
display: grid;
grid-template-columns: 0.7fr 1.5fr auto;
gap: 40px;
align-items: end;
min-height: 540px;
padding: clamp(44px, 7vw, 100px) clamp(20px, 4vw, 64px);
background: var(--ink);
color: var(--white);
}
.site-footer > div {
display: flex;
flex-direction: column;
gap: 7px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
.site-footer p {
margin: 0;
font-size: clamp(56px, 8vw, 130px);
font-weight: 800;
letter-spacing: -0.075em;
line-height: 0.78;
text-transform: uppercase;
}
.site-footer > a {
padding-bottom: 5px;
border-bottom: 1px solid currentColor;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
}
:focus-visible {
outline: 3px solid currentColor;
outline-offset: 4px;
}
@media (max-width: 1050px) {
.hero {
grid-template-columns: 1fr;
}
.hero__intro {
min-height: 740px;
border-right: 0;
border-bottom: var(--line);
}
.hero__status {
min-height: 560px;
}
.project-grid .project-card:first-child,
.project-grid .project-card:nth-child(2),
.project-grid .project-card:nth-child(3) {
grid-column: span 12;
margin-top: 0;
}
.project-grid .project-card:nth-child(3) {
width: calc(100% - 100px);
margin-left: 100px;
}
.section-heading__body {
grid-template-columns: 1fr;
}
}
@media (max-width: 760px) {
.site-header {
min-height: 74px;
}
.site-mark > span:last-child {
display: none;
}
.site-header nav {
gap: 16px;
font-size: 10px;
}
.site-header nav a:nth-child(2) {
display: none;
}
.site-header nav a:nth-child(3) {
font-size: 0;
}
.site-header nav a:nth-child(3)::after {
content: "Code";
font-size: 10px;
}
.hero__intro {
min-height: 700px;
}
.hero h1 {
font-size: clamp(56px, 16.5vw, 84px);
}
.hero__footer {
align-items: stretch;
flex-direction: column;
}
.brutal-button {
align-self: flex-start;
}
.hero__status {
min-height: 480px;
padding: 42px 26px;
}
.section-heading {
grid-template-columns: 1fr;
}
.section-heading__body {
gap: 24px;
}
.section-heading__body > p {
font-size: 15px;
}
.project-card {
min-height: 520px;
}
.project-card__body {
margin: 52px 0;
}
.project-card__footer {
align-items: flex-start;
flex-direction: column;
}
.project-grid .project-card:nth-child(3) {
width: 100%;
margin-left: 0;
}
.upcoming-grid .project-card {
grid-column: span 12;
}
.about-section {
grid-template-columns: 1fr;
}
.about-section__label {
min-height: 220px;
border-right: 0;
border-bottom: var(--line);
}
.about-section__columns,
.repo-links {
grid-template-columns: 1fr;
}
.site-footer {
grid-template-columns: 1fr;
min-height: 640px;
align-items: start;
}
.site-footer p {
align-self: center;
}
.site-footer > a {
justify-self: start;
}
}
@media (prefers-reduced-motion: reduce) {
html {
scroll-behavior: auto;
}
.ticker > div,
.project-card__status span {
animation: none;
}
.brutal-button,
.win98-host,
.project-card,
.repo-links > a {
transition: none;
}
}
+1 -1
View File
@@ -9,6 +9,6 @@ export const Route = createRootRoute({
function RootComponent() {
return <>
<Outlet />
<TanStackRouterDevtools />
{import.meta.env.DEV && <TanStackRouterDevtools />}
</>
}
+175 -1
View File
@@ -1,4 +1,8 @@
import { createFileRoute } from "@tanstack/react-router"
import { ArrowDown, ArrowUpRight, Code2, GitBranch } from "lucide-react"
import { ProjectCard } from "@/components/portfolio/ProjectCard"
import { SectionHeading } from "@/components/portfolio/SectionHeading"
import { Win98Status } from "@/components/portfolio/Win98Status"
export const Route = createFileRoute("/_main/")({
@@ -6,5 +10,175 @@ export const Route = createFileRoute("/_main/")({
})
function RouteComponent() {
return <p className="text-center">Coming soon!</p>
return (
<main>
<header className="site-header">
<a className="site-mark" href="#top" aria-label="Julien Valverde, back to top">
<span>JV</span>
<span>Full-stack engineer<br />&amp; product builder</span>
</a>
<nav aria-label="Main navigation">
<a href="#work">Work</a>
<a href="#about">About</a>
<a href="#repositories">Repositories</a>
</nav>
</header>
<section className="hero" id="top">
<div className="hero__intro">
<p className="hero__kicker">Independent engineer · Chambéry, France</p>
<h1>
I build
<span>useful</span>
systems.
</h1>
<div className="hero__footer">
<p>
From open-source primitives to vertical products software with strong types,
clear interfaces, and a reason to exist.
</p>
<a className="brutal-button" href="#work">
Explore selected work
<ArrowDown aria-hidden="true" />
</a>
</div>
</div>
<aside className="hero__status" aria-label="Developer status">
<div className="hero__stamp" aria-hidden="true">EST. 2019 / WEB</div>
<Win98Status />
</aside>
</section>
<div className="ticker" aria-hidden="true">
<div>
<span>Effect</span><i></i>
<span>TypeScript</span><i></i>
<span>Product engineering</span><i></i>
<span>Open source</span><i></i>
<span>Effect</span><i></i>
<span>TypeScript</span><i></i>
<span>Product engineering</span><i></i>
<span>Open source</span><i></i>
</div>
</div>
<section className="work-section" id="work">
<SectionHeading
eyebrow="01 / Selected work"
title="Tools for developers. Products for people."
description="A selection of open-source libraries, production software, and what is currently taking shape."
/>
<div className="project-grid">
<ProjectCard
index="01"
title="effect-lens"
category="Open source / library"
description="An effectful Lens for focusing, subscribing to, and updating nested state—without losing typed failures or requirements along the way."
tags={["Effect", "State", "TypeScript"]}
href="https://www.npmjs.com/package/effect-lens"
linkLabel="View on npm"
variant="dark"
/>
<ProjectCard
index="02"
title="effect-view"
category="Open source / React"
description="A small bridge for writing React function components with Effect, designed around composable programs instead of scattered side effects."
tags={["Effect", "React", "DX"]}
href="https://www.npmjs.com/package/effect-view"
linkLabel="View on npm"
variant="light"
/>
<ProjectCard
index="03"
title="Comme le Chef"
category="SaaS / Food tech"
description="An online recipe workspace for composing and balancing culinary creations, tracking nutrition and allergens, and generating production documents."
tags={["Product", "SaaS", "Food tech"]}
href="https://commedeschef.com/"
linkLabel="Visit website"
variant="outline"
/>
</div>
</section>
<section className="upcoming-section">
<SectionHeading
eyebrow="02 / On the workbench"
title="Two new systems are booting."
description="Early-stage products in active development. More details will appear when there is something worth showing."
/>
<div className="upcoming-grid">
<ProjectCard
index="04"
title="FLOWpti"
category="Upcoming / Logistics"
description="Delivery route optimization for transport professionals—turning operational constraints into clearer, more efficient routes."
tags={["Routing", "Optimization", "B2B"]}
/>
<ProjectCard
index="05"
title="Sentinel"
category="Upcoming / Open source"
description="A self-hosted telemetry and observability stack that synchronizes OBD-II vehicle data for visualization, diagnostics, and remote insight."
tags={["OBD-II", "Telemetry", "Self-hosted"]}
variant="dark"
/>
</div>
</section>
<section className="about-section" id="about">
<div className="about-section__label">
<span>03</span>
<span>About</span>
</div>
<div className="about-section__copy">
<p className="about-section__lead">
Im Julien Valverde, a full-stack engineer and entrepreneur based in the French Alps.
</p>
<div className="about-section__columns">
<p>
I like the difficult middle: where domain problems, product decisions,
interface design, and systems engineering all meet.
</p>
<p>
My current work spans typed functional programming, React, operational
software, optimization, and self-hosted infrastructure.
</p>
</div>
</div>
</section>
<section className="repo-section" id="repositories">
<p className="repo-section__eyebrow">Source code, experiments, and works in progress</p>
<h2>Follow the commits.</h2>
<div className="repo-links">
<a href="https://git.valverde.cloud/" target="_blank" rel="noreferrer">
<GitBranch aria-hidden="true" />
<span><small>Primary forge</small>git.valverde.cloud</span>
<ArrowUpRight aria-hidden="true" />
</a>
<a href="https://github.com/Thiladev" target="_blank" rel="noreferrer">
<Code2 aria-hidden="true" />
<span><small>Public collaboration</small>github.com/Thiladev</span>
<ArrowUpRight aria-hidden="true" />
</a>
</div>
</section>
<footer className="site-footer">
<div>
<span>Julien Valverde</span>
<span>© {new Date().getFullYear()}</span>
</div>
<p>Build it well.<br />Make it useful.</p>
<a href="#top">Back to top </a>
</footer>
</main>
)
}
+3 -102
View File
@@ -1,109 +1,10 @@
import { createFileRoute, Outlet } from "@tanstack/react-router"
import { useTranslation } from "react-i18next"
import { DiGit } from "react-icons/di"
import { FaCode } from "react-icons/fa"
import root from "react-shadow"
import { Button } from "@/components/ui/button"
import { Card, CardContent } from "@/components/ui/card"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip"
export const Route = createFileRoute("/_main")({
component: WIPLayout,
component: MainLayout,
})
function WIPLayout() {
return (
<div className="mx-auto w-[750px] max-w-full px-5 pt-28 pb-10 text-foreground">
<Card>
<CardContent>
<Outlet />
</CardContent>
</Card>
</div>
)
}
// @ts-expect-error
function SharedLayout() {
const { t, i18n } = useTranslation()
return <>
<div className="flex flex-row items-center justify-center gap-2 p-2">
<Button
variant={i18n.language !== "en" ? "default" : "noShadow"}
onClick={() => i18n.changeLanguage("en")}
>
🇬🇧 English
</Button>
<Button
variant={i18n.language !== "fr" ? "default" : "noShadow"}
onClick={() => i18n.changeLanguage("fr")}
>
🇫🇷 Français
</Button>
</div>
<div className="container mt-20 mr-auto ml-auto">
<div className="mr-auto ml-auto flex w-fit flex-row items-start justify-around gap-10">
<root.div>
<link rel="stylesheet" href="/98.css/dist/98.css" />
<div className="window" style={{ width: 300 }}>
<div className="title-bar">
<div className="title-bar-text">Julien Valverde</div>
</div>
<div className="window-body">
<p>There's so much room for activities!</p>
</div>
</div>
</root.div>
<div>
<h1>Ingénieur Full Stack &bull; Entrepreneur</h1>
<p className="mt-2">
I am passionate about integrating functionality and design in applications to create intuitive, user-friendly experiences.
</p>
<div className="mt-4 flex w-fit flex-row items-center justify-center gap-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button asChild>
<a href="https://git.valverde.cloud"><DiGit /> {t($ => $.gitRepos.title, { ns: "nav" })}</a>
</Button>
</TooltipTrigger>
<TooltipContent><p>{t($ => $.gitRepos.tooltip, { ns: "nav" })}</p></TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button asChild>
<a href="https://git.valverde.cloud/thilawyn/website"><FaCode /> {t($ => $.sourceCode.title, { ns: "nav" })}</a>
</Button>
</TooltipTrigger>
<TooltipContent><p>{t($ => $.sourceCode.tooltip, { ns: "nav" })}</p></TooltipContent>
</Tooltip>
</TooltipProvider>
<Button>Resumé</Button>
</div>
</div>
</div>
</div>
<div className="mx-auto w-[750px] max-w-full px-5 pt-28 pb-10 text-foreground">
<Card>
<CardContent>
<Outlet />
</CardContent>
</Card>
</div>
</>
function MainLayout() {
return <Outlet />
}
-1
View File
@@ -5,7 +5,6 @@
{ "path": "./tsconfig.node.json" }
],
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}