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

File diff suppressed because it is too large Load Diff

193
script.js
View File

@@ -1,89 +1,142 @@
// ─── 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;
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;
}
});
navLinks.forEach(a => {
a.style.color = a.getAttribute('href') === `#${current}` ? 'var(--mauve)' : '';
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;
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) {
return;
}
const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
if (openInNewTab) {
window.open(href, '_blank', 'noopener');
return;
}
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);
});
});
// ─── Project filters ──────────────────────────────────────────────
const filterChips = document.querySelectorAll('.filter-chip');
const projectCards = document.querySelectorAll('#project-grid .project-card');
const seeMoreButton = document.querySelector('#projects-see-more');
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 goToCardHref = (card, event) => {
const href = card.dataset.href;
if (!href) {
return;
}
const openInNewTab = event.metaKey || event.ctrlKey || event.button === 1;
if (openInNewTab) {
window.open(href, '_blank', 'noopener');
return;
}
globalThis.location.href = href;
};
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) {
card.classList.remove('is-collapsed');
visibleCards.push(card);
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) {
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';
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();
}

1371
style.css

File diff suppressed because it is too large Load Diff