Added open source links
This commit is contained in:
25
icons/docker-logo.svg
Normal file
25
icons/docker-logo.svg
Normal 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
6
icons/haskell-logo.svg
Normal 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
45
icons/java-icon.svg
Normal 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
1
icons/react-logo.svg
Normal 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 |
71
index.html
71
index.html
@@ -180,7 +180,13 @@
|
||||
|
||||
<div class="skills-grid">
|
||||
<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>
|
||||
<p>
|
||||
Professional experience designing, building and deploying services.
|
||||
@@ -195,7 +201,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<p>
|
||||
Comfortable with React and CSS. I've always enjoyed visualising
|
||||
@@ -208,7 +220,13 @@
|
||||
</div>
|
||||
</div>
|
||||
<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>
|
||||
<p>
|
||||
Long-term Linux & Vim user. Self-hosted web & media servers. Using
|
||||
@@ -225,7 +243,13 @@
|
||||
class="skill-card skill-card--wide reveal"
|
||||
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>
|
||||
<p>
|
||||
C, C++, x86_64 assembly, Haskell, R & MatLab from uni. Modules
|
||||
@@ -265,35 +289,45 @@
|
||||
class="card-grid card-grid--featured reveal"
|
||||
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="project-status project-status--active">Active development</span>
|
||||
<span class="card-title">Wordle Solver</span>
|
||||
<span class="card-desc"
|
||||
>Built after one too many missed 3-guess games. Now it plays
|
||||
marginally better than me. Uses information theory to recommend the
|
||||
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 tag-yellow">Rust</span>
|
||||
<span class="tag tag-teal">React</span>
|
||||
<span class="tag tag-peach">TypeScript</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</a>
|
||||
<a class="card" href="https://crackthequote.umbra.mom">
|
||||
</article>
|
||||
<article class="card card-link" data-href="https://crackthequote.umbra.mom">
|
||||
<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-desc"
|
||||
>A substitution-cipher puzzle game with a daily challenge. Built for
|
||||
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 tag-yellow">Rust</span>
|
||||
<span class="tag tag-teal">React</span>
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</a>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
<div class="projects-subheading reveal" style="transition-delay: 0.16s">
|
||||
@@ -343,6 +377,7 @@
|
||||
data-href="https://pubquiz.umbra.mom"
|
||||
>
|
||||
<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-desc"
|
||||
>A Python dashboard to track our local pub quiz performances.</span
|
||||
@@ -350,7 +385,6 @@
|
||||
<span class="tag-row">
|
||||
<span class="tag tag-red">Python</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -368,7 +402,6 @@
|
||||
<span class="tag tag-teal">WebAssembly</span>
|
||||
<span class="tag tag-peach">RayLib</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -387,7 +420,6 @@
|
||||
<span class="tag tag-sky">WebGL</span>
|
||||
<span class="tag tag-teal">WebAssembly</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -405,7 +437,6 @@
|
||||
<span class="tag tag-yellow">Rust</span>
|
||||
<span class="tag tag-peach">Raylib</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -413,6 +444,7 @@
|
||||
data-href="https://samstoreymusic.com"
|
||||
>
|
||||
<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-desc"
|
||||
>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-green">JavaScript</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -439,7 +470,6 @@
|
||||
<span class="tag tag-teal">React</span>
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -462,7 +492,6 @@
|
||||
<span class="tag tag-peach">RayLib</span>
|
||||
<span class="tag tag-teal">WebAssembly</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -484,7 +513,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -501,7 +529,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -517,7 +544,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -534,7 +560,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -556,7 +581,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -572,7 +596,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -588,7 +611,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
<article
|
||||
class="card project-card card-link"
|
||||
@@ -610,7 +632,6 @@
|
||||
<span class="tag tag-green">JavaScript</span>
|
||||
<span class="tag tag-red">p5.js</span>
|
||||
</span>
|
||||
<span class="card-arrow">→</span>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
161
script.js
161
script.js
@@ -1,39 +1,54 @@
|
||||
// ─── Scroll reveal ─────────────────────────────────────────
|
||||
const reveals = document.querySelectorAll('.reveal');
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach(e => {
|
||||
if (e.isIntersecting) {
|
||||
e.target.classList.add('visible');
|
||||
observer.unobserve(e.target);
|
||||
const revealObserver = new IntersectionObserver((entries, observer) => {
|
||||
entries.forEach((entry) => {
|
||||
if (!entry.isIntersecting) {
|
||||
return;
|
||||
}
|
||||
|
||||
entry.target.classList.add('visible');
|
||||
observer.unobserve(entry.target);
|
||||
});
|
||||
}, {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 ──────────────────────────────
|
||||
const sections = document.querySelectorAll('section[id]');
|
||||
const navLinks = document.querySelectorAll('.nav-links a');
|
||||
window.addEventListener('scroll', () => {
|
||||
let current = '';
|
||||
sections.forEach(s => {
|
||||
if (window.scrollY >= s.offsetTop - 120) current = s.id;
|
||||
});
|
||||
navLinks.forEach(a => {
|
||||
a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : '';
|
||||
});
|
||||
const sections = Array.from(document.querySelectorAll('section[id]'));
|
||||
const navLinks = Array.from(document.querySelectorAll('.nav-links a'));
|
||||
const updateActiveNavLink = () => {
|
||||
let currentSectionId = '';
|
||||
|
||||
sections.forEach((section) => {
|
||||
if (window.scrollY >= section.offsetTop - 120) {
|
||||
currentSectionId = section.id;
|
||||
}
|
||||
});
|
||||
|
||||
// ─── Project filters ──────────────────────────────────────────────
|
||||
const filterChips = document.querySelectorAll('.filter-chip');
|
||||
const projectCards = document.querySelectorAll('#project-grid .project-card');
|
||||
navLinks.forEach((link) => {
|
||||
const isActive = link.getAttribute('href') === `#${currentSectionId}`;
|
||||
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 cardLinks = document.querySelectorAll('.card-link[data-href]');
|
||||
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 isNestedInteractiveTarget = (card, target) => {
|
||||
if (!(target instanceof Element)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const interactiveAncestor = target.closest(interactiveSelector);
|
||||
return interactiveAncestor !== null && interactiveAncestor !== card;
|
||||
};
|
||||
|
||||
const goToCardHref = (card, event) => {
|
||||
const href = card.dataset.href;
|
||||
if (!href) {
|
||||
@@ -49,41 +64,79 @@ if (filterChips.length > 0 && projectCards.length > 0) {
|
||||
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 visibleCards = [];
|
||||
|
||||
projectCards.forEach(card => {
|
||||
const tech = (card.dataset.tech || '').split(/\s+/).filter(Boolean);
|
||||
projectCards.forEach((card) => {
|
||||
const tech = techByCard.get(card) || [];
|
||||
const shouldShow = selectedFilter === 'all' || tech.includes(selectedFilter);
|
||||
card.classList.toggle('is-hidden', !shouldShow);
|
||||
|
||||
if (shouldShow) {
|
||||
if (!shouldShow) {
|
||||
return;
|
||||
}
|
||||
|
||||
card.classList.remove('is-collapsed');
|
||||
visibleCards.push(card);
|
||||
}
|
||||
});
|
||||
|
||||
if (!isExpanded) {
|
||||
visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach(card => {
|
||||
visibleCards.slice(DEFAULT_VISIBLE_PROJECTS).forEach((card) => {
|
||||
card.classList.add('is-collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
if (seeMoreButton) {
|
||||
if (!seeMoreButton) {
|
||||
return;
|
||||
}
|
||||
|
||||
const canExpand = visibleCards.length > DEFAULT_VISIBLE_PROJECTS;
|
||||
seeMoreButton.hidden = !canExpand;
|
||||
seeMoreButton.setAttribute('aria-expanded', canExpand && isExpanded ? 'true' : 'false');
|
||||
seeMoreButton.textContent = isExpanded ? 'See less' : 'See more';
|
||||
}
|
||||
};
|
||||
|
||||
filterChips.forEach(chip => {
|
||||
filterChips.forEach((chip) => {
|
||||
chip.addEventListener('click', () => {
|
||||
selectedFilter = chip.dataset.filter || 'all';
|
||||
isExpanded = false;
|
||||
|
||||
filterChips.forEach(other => {
|
||||
other.classList.toggle('active', other === chip);
|
||||
filterChips.forEach((otherChip) => {
|
||||
otherChip.classList.toggle('active', otherChip === chip);
|
||||
});
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
|
||||
117
style.css
117
style.css
@@ -41,8 +41,7 @@ html {
|
||||
}
|
||||
|
||||
body {
|
||||
background:
|
||||
radial-gradient(rgba(205, 214, 244, 0.028) 0.5px, transparent 0.6px),
|
||||
background: 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(
|
||||
1200px 800px at 12% 8%,
|
||||
@@ -55,14 +54,12 @@ body {
|
||||
transparent 64%
|
||||
),
|
||||
linear-gradient(180deg, #1e1e2e 0%, #1b1b2b 55%, #1a1a29 100%);
|
||||
background-size:
|
||||
3px 3px,
|
||||
background-size: 3px 3px,
|
||||
3px 3px,
|
||||
auto,
|
||||
auto,
|
||||
auto;
|
||||
background-position:
|
||||
0 0,
|
||||
background-position: 0 0,
|
||||
1px 1px,
|
||||
0 0,
|
||||
0 0,
|
||||
@@ -191,6 +188,10 @@ nav {
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-links a.is-active {
|
||||
color: var(--mauve);
|
||||
}
|
||||
|
||||
.nav-links a:hover::after {
|
||||
transform: scaleX(1);
|
||||
}
|
||||
@@ -306,14 +307,6 @@ section.full-width {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.hero-by-footer {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
color: var(--subtext0);
|
||||
font-size: 0.82rem;
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.hero-links {
|
||||
display: flex;
|
||||
gap: 1.25rem;
|
||||
@@ -340,8 +333,7 @@ section.full-width {
|
||||
.btn-primary {
|
||||
background: var(--mauve);
|
||||
color: var(--crust);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(203, 166, 247, 0.25),
|
||||
box-shadow: 0 0 0 1px rgba(203, 166, 247, 0.25),
|
||||
0 10px 26px rgba(17, 17, 27, 0.4);
|
||||
}
|
||||
|
||||
@@ -367,8 +359,7 @@ section.full-width {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background-image:
|
||||
linear-gradient(var(--surface0) 1px, transparent 1px),
|
||||
background-image: linear-gradient(var(--surface0) 1px, transparent 1px),
|
||||
linear-gradient(90deg, var(--surface0) 1px, transparent 1px);
|
||||
background-size: 60px 60px;
|
||||
opacity: 0.18;
|
||||
@@ -483,8 +474,7 @@ section.full-width {
|
||||
border: 2px solid var(--surface1);
|
||||
flex-shrink: 0;
|
||||
margin-top: 0.15rem;
|
||||
transition:
|
||||
border-color 0.2s,
|
||||
transition: border-color 0.2s,
|
||||
background 0.2s;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -534,8 +524,7 @@ section.full-width {
|
||||
font-size: 0.7rem;
|
||||
color: var(--overlay1);
|
||||
margin-left: 0.3rem;
|
||||
transition:
|
||||
color 0.2s,
|
||||
transition: color 0.2s,
|
||||
transform 0.2s;
|
||||
display: inline-block;
|
||||
}
|
||||
@@ -603,6 +592,12 @@ section.full-width {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.skill-card-icon-image {
|
||||
width: 1.75rem;
|
||||
height: 1.75rem;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.skill-card h3 {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
@@ -815,18 +810,23 @@ section.full-width {
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: background 0.2s;
|
||||
transition: background 0.2s, transform 0.2s, box-shadow 0.2s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-link {
|
||||
cursor: pointer;
|
||||
box-shadow: inset 0 0 0 1px transparent;
|
||||
}
|
||||
|
||||
.card-link:focus-visible {
|
||||
outline: 1px solid var(--mauve);
|
||||
outline-offset: -1px;
|
||||
outline: none;
|
||||
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 {
|
||||
@@ -847,7 +847,15 @@ section.full-width {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -857,15 +865,37 @@ section.full-width {
|
||||
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 {
|
||||
font-size: 0.92rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
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);
|
||||
text-decoration-color: currentColor;
|
||||
}
|
||||
|
||||
.card-desc {
|
||||
@@ -875,6 +905,22 @@ section.full-width {
|
||||
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 {
|
||||
color: var(--sky);
|
||||
text-decoration: underline;
|
||||
@@ -885,20 +931,6 @@ section.full-width {
|
||||
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 {
|
||||
@@ -1030,8 +1062,7 @@ footer a:hover {
|
||||
.reveal {
|
||||
opacity: 0;
|
||||
transform: translateY(24px);
|
||||
transition:
|
||||
opacity 0.6s ease,
|
||||
transition: opacity 0.6s ease,
|
||||
transform 0.6s ease;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user