Added open source links

This commit is contained in:
John Gatward
2026-03-31 21:54:01 +01:00
parent 13c8b0a28e
commit 16bd425c20
7 changed files with 1348 additions and 1208 deletions

25
icons/docker-logo.svg Normal file
View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" viewBox="0 0 340 268">
<!-- Generator: Adobe Illustrator 30.1.0, SVG Export Plug-In . SVG Version: 2.1.1 Build 136) -->
<defs>
<style>
.st0 {
fill: none;
}
.st1 {
fill: #2560ff;
}
.st2 {
clip-path: url(#clippath);
}
</style>
<clipPath id="clippath">
<rect class="st0" width="339.5" height="268"/>
</clipPath>
</defs>
<g class="st2">
<path class="st1" d="M334,110.1c-8.3-5.6-30.2-8-46.1-3.7-.9-15.8-9-29.2-24-40.8l-5.5-3.7-3.7,5.6c-7.2,11-10.3,25.7-9.2,39,.8,8.2,3.7,17.4,9.2,24.1-20.7,12-39.8,9.3-124.3,9.3H0c-.4,19.1,2.7,55.8,26,85.6,2.6,3.3,5.4,6.5,8.5,9.6,19,19,47.6,32.9,90.5,33,65.4,0,121.4-35.3,155.5-120.8,11.2.2,40.8,2,55.3-26,.4-.5,3.7-7.4,3.7-7.4l-5.5-3.7h0ZM85.2,92.7h-36.7v36.7h36.7v-36.7ZM132.6,92.7h-36.7v36.7h36.7v-36.7ZM179.9,92.7h-36.7v36.7h36.7v-36.7ZM227.3,92.7h-36.7v36.7h36.7v-36.7ZM37.8,92.7H1.1v36.7h36.7v-36.7ZM85.2,46.3h-36.7v36.7h36.7v-36.7ZM132.6,46.3h-36.7v36.7h36.7v-36.7ZM179.9,46.3h-36.7v36.7h36.7v-36.7ZM179.9,0h-36.7v36.7h36.7V0Z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

6
icons/haskell-logo.svg Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 17 12">
<path fill="#453a62" d="M 0 12 L 4 6 L 0 0 L 3 0 L 7 6 L 3 12"/>
<path fill="#5e5086" d="M 4 12 L 8 6 L 4 0 L 7 0 L 15 12 L 12 12 L 9.5 8.25 L 7 12"/>
<path fill="#8f4e8b" d="M 13.66 8.5 L 12.333 6.5 L 17 6.5 L 17 8.5 M 11.666 5.5 L 10.333 3.5 L 17 3.5 L 17 5.5"/>
</svg>

After

Width:  |  Height:  |  Size: 377 B

45
icons/java-icon.svg Normal file
View File

@@ -0,0 +1,45 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 28.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Слой_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 724.6950073 1000" enable-background="new 0 0 724.6950073 1000" xml:space="preserve">
<g id="Group">
<path id="Vector_2" fill="#2978AE" d="M93.918396,594.6341553c0-27.6937866,105.210907-43.125,154.1508789-46.8843994
l4.7553253,2.7719116c-18.8231354,3.3624878-94.3134155,16.6156006-94.3134155,34.0218506
c0,18.7937622,115.7121429,31.2562866,183.0792999,31.2562866c114.1280823,0,191.7967834-17.2094116,212.5999451-22.9468994
l29.1281128,17.0124512c-20.0125122,9.6937256-105.8062134,35.4093628-241.7280579,35.4093628
c-151.1789856,0-247.4739685-29.4749146-247.4739685-50.4437256L93.918396,594.6341553z M324.1530151,983.340271
c-59.8365479,0.59375-132.7509003-4.3500366-194.1736908-14.6375122l-5.7459335,3.3624878
c61.2246628,18.0031128,146.4236755,28.6843262,239.9445801,27.8937378
c183.8718262-1.5843506,332.8717041-47.0812378,335.8436279-101.874939l-2.1812744-1.1875
c-12.2843628,15.0343628-91.9343262,84.2687378-373.6873169,86.6437378V983.340271z M339.8061218,948.1309204
c150.3874512-1.3843994,318.803009-30.6625366,318.4092712-80.1156006c0-8.9031372-5.9468994-15.0343628-10.9000244-18.7937622
l-2.3781128,1.3843994c-13.8687134,38.1780396-131.562439,66.4655762-305.131134,68.0499268
c-112.1449585,0.9875488-267.2864685-25.9156494-267.4845886-56.7749634
c-0.1981201-31.0562744,73.5090485-48.0687256,73.5090485-48.0687256l-5.1515808-2.9656372
C90.7480927,817.5715942-0.3949873,841.1121826,0.001288,875.3340454
C0.3975623,924.7871704,209.8286743,949.1183472,339.8061218,948.1309204z M678.8184814,607.8872681
c-2.9718628,57.9593506-56.6655884,93.9624634-110.3624878,124.624939l4.9562378,2.7687378
c57.2593994-16.0250244,159.2999878-62.906189,150.78125-134.7124023
c-4.1594238-35.8062744-37.0531006-61.5218506-79.8499756-61.5218506c-13.2749634,0-25.1624756,2.375-34.8718872,5.3406372
l-1.9812622,5.1436768C645.7310181,542.0153809,680.8028564,569.9060059,678.8184814,607.8872681z M236.775528,780.184082
c-17.4362488,3.5625-55.4784088,12.265625-55.4784088,30.6624756c0,25.5187378,81.4343262,45.1000366,160.0964966,45.1000366
c108.1812134,0,152.3655701-27.6937866,154.546814-29.2750244l-44.9780884-25.9155884
c-19.2187805,4.5499878-51.3187561,11.671814-109.3718567,11.671814c-64.7911987,0-106.9946289-11.078125-106.9946289-23.1437378
c0-2.5718384,1.5852966-5.5374756,4.5571899-7.9124756l-2.1793823-1.3843384L236.775528,780.184082z M502.4779358,693.9371948
c-24.9656067,7.1219482-80.8406067,18.3969116-161.0843201,18.3969116c-80.2471313,0-142.8587036-13.453125-143.0568237-29.2781372
c0-10.484314,12.6809387-15.0343628,12.6809387-15.0343628l-2.1796875-1.3843994
c-37.6459198,6.7282104-72.7162323,17.0125732-72.5180969,32.2437744
c0.3962402,27.8937378,107.1921387,48.8624268,205.0736694,48.8624268c83.2155762,0,162.8686829-13.8468628,198.9280396-32.2437134
l-37.8437195-21.7593384V693.9371948z"/>
<path id="Vector_3" fill="#F29111" d="M481.0810547,71.6093521c0,153.9005737-211.2133484,212.6517944-211.2133484,322.0436401
c0,76.7531128,50.9227905,125.0187073,79.2540283,155.2843323l-2.3781128,1.3874512
c-35.6631165-22.15625-129.3821411-78.1374512-129.3821411-170.5186768
c0-129.5693207,242.5195923-191.6833496,242.5195923-338.8582764c0-18.1989956-2.7749939-32.0460854-4.5562439-39.5630913
L457.7029419,0c7.7250061,9.6929655,23.1812439,33.8265495,23.1812439,71.6093521H481.0810547z M550.4310303,207.7064819
l-2.5749512-1.3846741c-45.9687805,15.429657-187.4405823,71.2137146-187.4405823,175.0655518
c0,58.7531128,57.4624634,91.3937073,57.4624634,146.3843079c0,19.5843506-11.0968628,38.1781006-20.2124939,49.0593262
l4.5593567,2.5718994c23.9718933-15.6281128,66.375-49.2562256,66.375-92.5780945
c0-36.7937317-50.921875-80.9062195-50.921875-128.1843262c0-74.3793335,98.2749939-132.9327393,132.7530823-151.1318054
V207.7064819z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

1
icons/react-logo.svg Normal file
View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="800" width="1200" viewBox="-73.59 -109.225 637.78 655.35"><g transform="translate(-175.7 -78)" fill="#61DAFB"><path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6z"/><circle r="45.7" cy="296.5" cx="420.9"/></g></svg>

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@@ -180,7 +180,13 @@
<div class="skills-grid"> <div class="skills-grid">
<div class="skill-card reveal"> <div class="skill-card reveal">
<span class="skill-card-icon"></span> <span class="skill-card-icon">
<img
src="icons/java-icon.svg"
alt="Java"
class="skill-card-icon-image"
/>
</span>
<h3>Backend</h3> <h3>Backend</h3>
<p> <p>
Professional experience designing, building and deploying services. Professional experience designing, building and deploying services.
@@ -195,7 +201,13 @@
</div> </div>
</div> </div>
<div class="skill-card reveal" style="transition-delay: 0.08s"> <div class="skill-card reveal" style="transition-delay: 0.08s">
<span class="skill-card-icon">🎨</span> <span class="skill-card-icon">
<img
src="icons/react-logo.svg"
alt="React"
class="skill-card-icon-image"
/>
</span>
<h3>Frontend & Creative</h3> <h3>Frontend & Creative</h3>
<p> <p>
Comfortable with React and CSS. I've always enjoyed visualising Comfortable with React and CSS. I've always enjoyed visualising
@@ -208,7 +220,13 @@
</div> </div>
</div> </div>
<div class="skill-card reveal" style="transition-delay: 0.16s"> <div class="skill-card reveal" style="transition-delay: 0.16s">
<span class="skill-card-icon">🖥</span> <span class="skill-card-icon">
<img
src="icons/docker-logo.svg"
alt="React"
class="skill-card-icon-image"
/>
</span>
<h3>Systems & Infra</h3> <h3>Systems & Infra</h3>
<p> <p>
Long-term Linux & Vim user. Self-hosted web & media servers. Using Long-term Linux & Vim user. Self-hosted web & media servers. Using
@@ -225,7 +243,13 @@
class="skill-card skill-card--wide reveal" class="skill-card skill-card--wide reveal"
style="transition-delay: 0.24s" style="transition-delay: 0.24s"
> >
<span class="skill-card-icon">🎓</span> <span class="skill-card-icon">
<img
src="icons/haskell-logo.svg"
alt="React"
class="skill-card-icon-image"
/>
</span>
<h3>Academic Breadth</h3> <h3>Academic Breadth</h3>
<p> <p>
C, C++, x86_64 assembly, Haskell, R &amp; MatLab from uni. Modules C, C++, x86_64 assembly, Haskell, R &amp; MatLab from uni. Modules
@@ -265,35 +289,45 @@
class="card-grid card-grid--featured reveal" class="card-grid card-grid--featured reveal"
style="transition-delay: 0.14s" style="transition-delay: 0.14s"
> >
<a class="card" href="https://wordlesolver.umbra.mom"> <article class="card card-link" data-href="https://wordlesolver.umbra.mom">
<span class="card-date">9 Feb 2025</span> <span class="card-date">9 Feb 2025</span>
<span class="project-status project-status--active">Active development</span>
<span class="card-title">Wordle Solver</span> <span class="card-title">Wordle Solver</span>
<span class="card-desc" <span class="card-desc"
>Built after one too many missed 3-guess games. Now it plays >Built after one too many missed 3-guess games. Now it plays
marginally better than me. Uses information theory to recommend the marginally better than me. Uses information theory to recommend the
next best guess.</span next best guess.</span
> >
<span class="card-source">
Source code:
<a href="https://github.com/jayo60013/wordle-solver-api" target="_blank" rel="noopener noreferrer">API</a>
·
<a href="https://github.com/jayo60013/wordle-solver-frontend" target="_blank" rel="noopener noreferrer">Frontend</a>
</span>
<span class="tag-row"> <span class="tag-row">
<span class="tag tag-yellow">Rust</span> <span class="tag tag-yellow">Rust</span>
<span class="tag tag-teal">React</span> <span class="tag tag-teal">React</span>
<span class="tag tag-peach">TypeScript</span> <span class="tag tag-peach">TypeScript</span>
</span> </span>
<span class="card-arrow"></span> </article>
</a> <article class="card card-link" data-href="https://crackthequote.umbra.mom">
<a class="card" href="https://crackthequote.umbra.mom">
<span class="card-date">13 Apr 2024</span> <span class="card-date">13 Apr 2024</span>
<span class="project-status project-status--active">Active development</span>
<span class="card-title">Crack the Quote</span> <span class="card-title">Crack the Quote</span>
<span class="card-desc" <span class="card-desc"
>A substitution-cipher puzzle game with a daily challenge. Built for >A substitution-cipher puzzle game with a daily challenge. Built for
me and my family to play after getting bored with NYT games.</span me and my family to play after getting bored with NYT games.</span
> >
<span class="card-source">
Source code:
<a href="https://github.com/jayo60013/crack-the-quote-api" target="_blank" rel="noopener noreferrer">API</a>
</span>
<span class="tag-row"> <span class="tag-row">
<span class="tag tag-yellow">Rust</span> <span class="tag tag-yellow">Rust</span>
<span class="tag tag-teal">React</span> <span class="tag tag-teal">React</span>
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
</span> </span>
<span class="card-arrow"></span> </article>
</a>
</div> </div>
<div class="projects-subheading reveal" style="transition-delay: 0.16s"> <div class="projects-subheading reveal" style="transition-delay: 0.16s">
@@ -343,6 +377,7 @@
data-href="https://pubquiz.umbra.mom" data-href="https://pubquiz.umbra.mom"
> >
<span class="card-date">03 Apr 2025</span> <span class="card-date">03 Apr 2025</span>
<span class="project-status project-status--active">Active development</span>
<span class="card-title">Pub Quiz Dashboard</span> <span class="card-title">Pub Quiz Dashboard</span>
<span class="card-desc" <span class="card-desc"
>A Python dashboard to track our local pub quiz performances.</span >A Python dashboard to track our local pub quiz performances.</span
@@ -350,7 +385,6 @@
<span class="tag-row"> <span class="tag-row">
<span class="tag tag-red">Python</span> <span class="tag tag-red">Python</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -368,7 +402,6 @@
<span class="tag tag-teal">WebAssembly</span> <span class="tag tag-teal">WebAssembly</span>
<span class="tag tag-peach">RayLib</span> <span class="tag tag-peach">RayLib</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -387,7 +420,6 @@
<span class="tag tag-sky">WebGL</span> <span class="tag tag-sky">WebGL</span>
<span class="tag tag-teal">WebAssembly</span> <span class="tag tag-teal">WebAssembly</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -405,7 +437,6 @@
<span class="tag tag-yellow">Rust</span> <span class="tag tag-yellow">Rust</span>
<span class="tag tag-peach">Raylib</span> <span class="tag tag-peach">Raylib</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -413,6 +444,7 @@
data-href="https://samstoreymusic.com" data-href="https://samstoreymusic.com"
> >
<span class="card-date">1 Nov 2023</span> <span class="card-date">1 Nov 2023</span>
<span class="project-status project-status--active">Active development</span>
<span class="card-title">samstoreymusic.com</span> <span class="card-title">samstoreymusic.com</span>
<span class="card-desc" <span class="card-desc"
>A website design and build for a friend working in music. Saving >A website design and build for a friend working in music. Saving
@@ -422,7 +454,6 @@
<span class="tag tag-mauve">HTML/CSS</span> <span class="tag tag-mauve">HTML/CSS</span>
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -439,7 +470,6 @@
<span class="tag tag-teal">React</span> <span class="tag tag-teal">React</span>
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -462,7 +492,6 @@
<span class="tag tag-peach">RayLib</span> <span class="tag tag-peach">RayLib</span>
<span class="tag tag-teal">WebAssembly</span> <span class="tag tag-teal">WebAssembly</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -484,7 +513,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -501,7 +529,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -517,7 +544,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -534,7 +560,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -556,7 +581,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -572,7 +596,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -588,7 +611,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
<article <article
class="card project-card card-link" class="card project-card card-link"
@@ -610,7 +632,6 @@
<span class="tag tag-green">JavaScript</span> <span class="tag tag-green">JavaScript</span>
<span class="tag tag-red">p5.js</span> <span class="tag tag-red">p5.js</span>
</span> </span>
<span class="card-arrow"></span>
</article> </article>
</div> </div>

161
script.js
View File

@@ -1,39 +1,54 @@
// ─── Scroll reveal ───────────────────────────────────────── // ─── Scroll reveal ─────────────────────────────────────────
const reveals = document.querySelectorAll('.reveal'); const reveals = document.querySelectorAll('.reveal');
const observer = new IntersectionObserver((entries) => { const revealObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(e => { entries.forEach((entry) => {
if (e.isIntersecting) { if (!entry.isIntersecting) {
e.target.classList.add('visible'); return;
observer.unobserve(e.target);
} }
entry.target.classList.add('visible');
observer.unobserve(entry.target);
}); });
}, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'}); }, {threshold: 0.1, rootMargin: '0px 0px -40px 0px'});
reveals.forEach(el => observer.observe(el)); reveals.forEach((element) => revealObserver.observe(element));
// ─── Active nav link on scroll ────────────────────────────── // ─── Active nav link on scroll ──────────────────────────────
const sections = document.querySelectorAll('section[id]'); const sections = Array.from(document.querySelectorAll('section[id]'));
const navLinks = document.querySelectorAll('.nav-links a'); const navLinks = Array.from(document.querySelectorAll('.nav-links a'));
window.addEventListener('scroll', () => { const updateActiveNavLink = () => {
let current = ''; let currentSectionId = '';
sections.forEach(s => {
if (window.scrollY >= s.offsetTop - 120) current = s.id; sections.forEach((section) => {
}); if (window.scrollY >= section.offsetTop - 120) {
navLinks.forEach(a => { currentSectionId = section.id;
a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : ''; }
});
}); });
// ─── Project filters ────────────────────────────────────────────── navLinks.forEach((link) => {
const filterChips = document.querySelectorAll('.filter-chip'); const isActive = link.getAttribute('href') === `#${currentSectionId}`;
const projectCards = document.querySelectorAll('#project-grid .project-card'); link.classList.toggle('is-active', isActive);
});
};
window.addEventListener('scroll', updateActiveNavLink, {passive: true});
updateActiveNavLink();
// ─── Project card links + filters ───────────────────────────
const filterChips = Array.from(document.querySelectorAll('.filter-chip'));
const projectCards = Array.from(document.querySelectorAll('#project-grid .project-card'));
const seeMoreButton = document.querySelector('#projects-see-more'); const seeMoreButton = document.querySelector('#projects-see-more');
const cardLinks = document.querySelectorAll('.card-link[data-href]');
const DEFAULT_VISIBLE_PROJECTS = 9; const DEFAULT_VISIBLE_PROJECTS = 9;
if (filterChips.length > 0 && projectCards.length > 0) {
let selectedFilter = 'all';
let isExpanded = false;
const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]'; const interactiveSelector = 'a, button, input, textarea, select, summary, [role="button"]';
const isNestedInteractiveTarget = (card, target) => {
if (!(target instanceof Element)) {
return false;
}
const interactiveAncestor = target.closest(interactiveSelector);
return interactiveAncestor !== null && interactiveAncestor !== card;
};
const goToCardHref = (card, event) => { const goToCardHref = (card, event) => {
const href = card.dataset.href; const href = card.dataset.href;
if (!href) { if (!href) {
@@ -49,41 +64,79 @@ if (filterChips.length > 0 && projectCards.length > 0) {
globalThis.location.href = href; globalThis.location.href = href;
}; };
cardLinks.forEach((card) => {
card.setAttribute('role', 'link');
card.setAttribute('tabindex', '0');
card.addEventListener('click', (event) => {
if (isNestedInteractiveTarget(card, event.target)) {
return;
}
goToCardHref(card, event);
});
card.addEventListener('keydown', (event) => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
if (isNestedInteractiveTarget(card, event.target)) {
return;
}
event.preventDefault();
goToCardHref(card, event);
});
});
if (filterChips.length > 0 && projectCards.length > 0) {
let selectedFilter = 'all';
let isExpanded = false;
const techByCard = new Map(
projectCards.map((card) => [card, (card.dataset.tech || '').split(/\s+/).filter(Boolean)]),
);
const applyFilter = () => { const applyFilter = () => {
const visibleCards = []; const visibleCards = [];
projectCards.forEach(card => { projectCards.forEach((card) => {
const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean); const tech = techByCard.get(card) || [];
const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter); const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter);
card.classList.toggle('is-hidden', !shouldShow); card.classList.toggle('is-hidden', !shouldShow);
if (shouldShow) { if (!shouldShow) {
return;
}
card.classList.remove('is-collapsed'); card.classList.remove('is-collapsed');
visibleCards.push(card); visibleCards.push(card);
}
}); });
if (!isExpanded) { if (!isExpanded) {
visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach(card => { visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => {
card.classList.add('is-collapsed'); card.classList.add('is-collapsed');
}); });
} }
if (seeMoreButton) { if (!seeMoreButton) {
return;
}
const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS; const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
seeMoreButton.hidden = !canExpand; seeMoreButton.hidden = !canExpand;
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false'); seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more'; seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
}
}; };
filterChips.forEach(chip => { filterChips.forEach((chip) => {
chip.addEventListener('click', () => { chip.addEventListener('click', () => {
selectedFilter = chip.dataset.filter || 'all'; selectedFilter = chip.dataset.filter || 'all';
isExpanded = false; isExpanded = false;
filterChips.forEach(other => { filterChips.forEach((otherChip) => {
other.classList.toggle('active', other === chip); otherChip.classList.toggle('active', otherChip === chip);
}); });
applyFilter(); applyFilter();
@@ -97,47 +150,5 @@ if (filterChips.length > 0 && projectCards.length > 0) {
}); });
} }
projectCards.forEach(card => {
if (!card.dataset.href) {
return;
}
card.setAttribute('role', 'link');
card.setAttribute('tabindex', '0');
card.addEventListener('click', event => {
const target = event.target;
if (!(target instanceof Element)) {
return;
}
// Let inline links and other controls inside the card handle their own clicks.
if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) {
return;
}
goToCardHref(card, event);
});
card.addEventListener('keydown', event => {
if (event.key !== 'Enter' && event.key !== ' ') {
return;
}
const target = event.target;
if (!(target instanceof Element)) {
return;
}
if (target.closest(interactiveSelector) && target.closest(interactiveSelector) !== card) {
return;
}
event.preventDefault();
goToCardHref(card, event);
});
});
applyFilter(); applyFilter();
} }

117
style.css
View File

@@ -41,8 +41,7 @@ html {
} }
body { body {
background: background: radial-gradient(rgba(205, 214, 244, 0.028) 0.5px, transparent 0.6px),
radial-gradient(rgba(205, 214, 244, 0.028) 0.5px, transparent 0.6px),
radial-gradient(rgba(17, 17, 27, 0.032) 0.5px, transparent 0.6px), radial-gradient(rgba(17, 17, 27, 0.032) 0.5px, transparent 0.6px),
radial-gradient( radial-gradient(
1200px 800px at 12% 8%, 1200px 800px at 12% 8%,
@@ -55,14 +54,12 @@ body {
transparent 64% transparent 64%
), ),
linear-gradient(180deg, #1e1e2e 0%, #1b1b2b 55%, #1a1a29 100%); linear-gradient(180deg, #1e1e2e 0%, #1b1b2b 55%, #1a1a29 100%);
background-size: background-size: 3px 3px,
3px 3px,
3px 3px, 3px 3px,
auto, auto,
auto, auto,
auto; auto;
background-position: background-position: 0 0,
0 0,
1px 1px, 1px 1px,
0 0, 0 0,
0 0, 0 0,
@@ -191,6 +188,10 @@ nav {
color: var(--text); color: var(--text);
} }
.nav-links a.is-active {
color: var(--mauve);
}
.nav-links a:hover::after { .nav-links a:hover::after {
transform: scaleX(1); transform: scaleX(1);
} }
@@ -306,14 +307,6 @@ section.full-width {
opacity: 0.8; opacity: 0.8;
} }
.hero-by-footer {
display: block;
margin-top: 1rem;
color: var(--subtext0);
font-size: 0.82rem;
opacity: 0.75;
}
.hero-links { .hero-links {
display: flex; display: flex;
gap: 1.25rem; gap: 1.25rem;
@@ -340,8 +333,7 @@ section.full-width {
.btn-primary { .btn-primary {
background: var(--mauve); background: var(--mauve);
color: var(--crust); color: var(--crust);
box-shadow: box-shadow: 0 0 0 1px rgba(203, 166, 247, 0.25),
0 0 0 1px rgba(203, 166, 247, 0.25),
0 10px 26px rgba(17, 17, 27, 0.4); 0 10px 26px rgba(17, 17, 27, 0.4);
} }
@@ -367,8 +359,7 @@ section.full-width {
content: ""; content: "";
position: absolute; position: absolute;
inset: 0; inset: 0;
background-image: background-image: linear-gradient(var(--surface0) 1px, transparent 1px),
linear-gradient(var(--surface0) 1px, transparent 1px),
linear-gradient(90deg, var(--surface0) 1px, transparent 1px); linear-gradient(90deg, var(--surface0) 1px, transparent 1px);
background-size: 60px 60px; background-size: 60px 60px;
opacity: 0.18; opacity: 0.18;
@@ -483,8 +474,7 @@ section.full-width {
border: 2px solid var(--surface1); border: 2px solid var(--surface1);
flex-shrink: 0; flex-shrink: 0;
margin-top: 0.15rem; margin-top: 0.15rem;
transition: transition: border-color 0.2s,
border-color 0.2s,
background 0.2s; background 0.2s;
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -534,8 +524,7 @@ section.full-width {
font-size: 0.7rem; font-size: 0.7rem;
color: var(--overlay1); color: var(--overlay1);
margin-left: 0.3rem; margin-left: 0.3rem;
transition: transition: color 0.2s,
color 0.2s,
transform 0.2s; transform 0.2s;
display: inline-block; display: inline-block;
} }
@@ -603,6 +592,12 @@ section.full-width {
display: block; display: block;
} }
.skill-card-icon-image {
width: 1.75rem;
height: 1.75rem;
display: block;
}
.skill-card h3 { .skill-card h3 {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
@@ -815,18 +810,23 @@ section.full-width {
gap: 0.5rem; gap: 0.5rem;
text-decoration: none; text-decoration: none;
color: inherit; color: inherit;
transition: background 0.2s; transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
} }
.card-link { .card-link {
cursor: pointer; cursor: pointer;
box-shadow: inset 0 0 0 1px transparent;
} }
.card-link:focus-visible { .card-link:focus-visible {
outline: 1px solid var(--mauve); outline: none;
outline-offset: -1px; background: var(--surface0);
transform: translateY(-2px);
box-shadow:
inset 0 0 0 1px rgba(203, 166, 247, 0.28),
0 14px 30px rgba(17, 17, 27, 0.28);
} }
.card::before { .card::before {
@@ -847,7 +847,15 @@ section.full-width {
transform: translateY(-1px); transform: translateY(-1px);
} }
.card:hover::before { .card-link:hover {
box-shadow:
inset 0 0 0 1px rgba(203, 166, 247, 0.18),
0 12px 24px rgba(17, 17, 27, 0.22);
transform: translateY(-2px);
}
.card:hover::before,
.card:focus-visible::before {
transform: scaleY(1); transform: scaleY(1);
} }
@@ -857,15 +865,37 @@ section.full-width {
letter-spacing: 0.06em; letter-spacing: 0.06em;
} }
.project-status {
display: inline-block;
align-self: flex-start;
font-size: 0.64rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 0.12rem 0.45rem;
border-radius: 999px;
}
.project-status--active {
color: var(--green);
background: rgba(166, 227, 161, 0.12);
border: 1px solid rgba(166, 227, 161, 0.24);
}
.card-title { .card-title {
font-size: 0.92rem; font-size: 0.92rem;
font-weight: 700; font-weight: 700;
color: var(--text); color: var(--text);
transition: color 0.2s; transition: color 0.2s;
text-decoration: underline;
text-decoration-color: transparent;
text-underline-offset: 0.18em;
} }
.card:hover .card-title { .card:hover .card-title,
.card:focus-visible .card-title {
color: var(--mauve); color: var(--mauve);
text-decoration-color: currentColor;
} }
.card-desc { .card-desc {
@@ -875,6 +905,22 @@ section.full-width {
flex: 1; flex: 1;
} }
.card-source {
font-size: 0.7rem;
color: var(--overlay1);
letter-spacing: 0.04em;
}
.card-source a {
color: var(--sky);
text-decoration: underline;
text-underline-offset: 2px;
}
.card-source a:hover {
color: var(--mauve);
}
.card-desc a { .card-desc a {
color: var(--sky); color: var(--sky);
text-decoration: underline; text-decoration: underline;
@@ -885,20 +931,6 @@ section.full-width {
color: var(--mauve); color: var(--mauve);
} }
.card-arrow {
font-size: 0.8rem;
color: var(--overlay0);
transition:
color 0.2s,
transform 0.2s;
align-self: flex-end;
margin-top: 0.5rem;
}
.card:hover .card-arrow {
color: var(--mauve);
transform: translateX(3px);
}
/* ─── Contact ──────────────────────────────────────────── */ /* ─── Contact ──────────────────────────────────────────── */
#contact { #contact {
@@ -1030,8 +1062,7 @@ footer a:hover {
.reveal { .reveal {
opacity: 0; opacity: 0;
transform: translateY(24px); transform: translateY(24px);
transition: transition: opacity 0.6s ease,
opacity 0.6s ease,
transform 0.6s ease; transform 0.6s ease;
} }