Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 5 additions & 20 deletions .github/workflows/static.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
name: Deploy to GitHub Pages
# The site is hosted on Cloudflare Pages, which builds from main on its own.
# This workflow only proves the site builds, on every push to main and on
# every pull request, so a broken build is caught before it reaches Cloudflare.
name: Build

on:
push:
branches: ["main"]
pull_request:
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: "pages"
cancel-in-progress: false

jobs:
build:
Expand All @@ -25,16 +23,3 @@ jobs:
cache: npm
- run: npm ci
- run: npm run build
- uses: actions/upload-pages-artifact@v3
with:
path: ./dist

deploy:
needs: build
runs-on: ubuntu-latest
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- uses: actions/deploy-pages@v2
id: deployment
121 changes: 121 additions & 0 deletions src/components/ProjectCard.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
---
/**
* One project card, as shown on /projects/ and in the homepage's Selected Work
* band. Sharp corners with a left accent border on hover, matching the
* post-preview and project-details-panel treatment used elsewhere on the site.
*/
import type { CollectionEntry } from 'astro:content';

interface Props {
project: CollectionEntry<'projects'>;
}

const { project } = Astro.props;
const techs = project.data.tech?.split(',').map((t) => t.trim()) ?? [];
---

<div class="card project-card h-100 overflow-hidden">
<div class="position-relative">
<div class="position-absolute top-0 start-0 w-100 h-100 bg-gradient-overlay"></div>
<img src={project.data.image} alt={`${project.data.title}`} class="card-img-top project-image" loading="lazy" />
<div class="position-absolute top-0 start-0 w-100 h-100 bg-gradient-bottom"></div>
<div class="position-absolute bottom-0 start-0 w-100 p-3 p-md-4 content-overlay">
<h3 class="h5 fw-bold text-white mb-2">{project.data.title}</h3>
<div class="d-flex flex-wrap gap-1 mb-2">
{techs.map((tech) => (
<span class="project-tech-badge">{tech}</span>
))}
</div>
<p class="text-white-50 small d-none d-md-block project-description">
{project.data.description}
</p>
<a href={`/projects/${project.slug}/`} class="btn btn-sm project-btn-primary mt-2 d-inline-flex align-items-center learn-more-btn">
Learn More
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="ms-1" viewBox="0 0 16 16">
<path fill-rule="evenodd" d="M1 8a.5.5 0 0 1 .5-.5h11.793l-3.147-3.146a.5.5 0 0 1 .708-.708l4 4a.5.5 0 0 1 0 .708l-4 4a.5.5 0 0 1-.708-.708L13.293 8.5H1.5A.5.5 0 0 1 1 8z"/>
</svg>
</a>
{project.data.webUrl && (
<a href={project.data.webUrl} class="btn btn-sm project-btn-ghost mt-2 ms-2 d-inline-flex align-items-center learn-more-btn">
Visit Page
</a>
)}
{project.data.github && (
<a href={project.data.github} class="btn btn-sm project-btn-ghost mt-2 ms-2 d-inline-flex align-items-center learn-more-btn">
GitHub
</a>
)}
</div>
</div>
</div>

<style>
/* Sharp corners + left-accent-border on hover instead of Bootstrap's default
rounded-card-with-shadow look. */
.project-card {
--bs-card-border-radius: 0;
--bs-card-inner-border-radius: 0;
border-radius: 0;
border: 1px solid var(--border-color);
border-left: 3px solid transparent;
box-shadow: none;
transition: border-color 0.3s ease, box-shadow 0.3s ease;
}
.project-card:hover {
border-left-color: var(--primary-color);
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.12);
}
.project-image {
height: 240px;
object-fit: cover;
border-radius: 0;
transition: transform 0.5s;
filter: saturate(1.1) contrast(1.05);
}
.project-card:hover .project-image { transform: scale(1.06); }
.bg-gradient-overlay {
background: linear-gradient(135deg, rgba(102, 204, 138, 0.18), rgba(55, 124, 251, 0.18));
z-index: 1;
}
.bg-gradient-bottom {
background: linear-gradient(to top, rgba(30, 37, 50, 0.92) 0%, rgba(30, 37, 50, 0.55) 50%, transparent 100%);
z-index: 2;
}
.content-overlay { z-index: 3; }
.project-description { display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden; }

.project-tech-badge {
display: inline-block;
padding: 0.2rem 0.55rem;
background: rgba(255, 255, 255, 0.12);
border: 1px solid rgba(255, 255, 255, 0.3);
color: white;
font-size: 0.7rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}

.project-btn-primary {
background: linear-gradient(135deg, var(--primary-color), var(--secondary-color));
color: white !important;
border: none;
border-radius: 0;
font-weight: 600;
}
.project-btn-primary:hover { color: white !important; filter: brightness(1.08); }
.project-btn-ghost {
background: rgba(255, 255, 255, 0.12);
color: white !important;
border: 1px solid rgba(255, 255, 255, 0.4);
border-radius: 0;
font-weight: 600;
}
.project-btn-ghost:hover { background: rgba(255, 255, 255, 0.22); color: white !important; }

.learn-more-btn { opacity: 0; transform: translateY(6px); transition: all 0.25s; }
.project-card:hover .learn-more-btn { opacity: 1; transform: translateY(0); }
@media (max-width: 768px) {
.learn-more-btn { opacity: 1; transform: translateY(0); }
}
</style>
57 changes: 57 additions & 0 deletions src/components/SectionScene.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
---
/**
* A small outline diagram for one of the homepage's colour-washed sections.
* The scene itself lives in src/scripts/sectionScenes.ts; this component only
* provides the canvas and wires it to the nearest `.reveal-section`, whose
* position in the viewport drives the reveal.
*/
import type { SceneName } from '../scripts/sectionScenes';

interface Props {
name: SceneName;
}

const { name } = Astro.props;
---

<div class="section-scene">
<canvas data-section-scene={name} aria-hidden="true"></canvas>
</div>

<style>
.section-scene {
width: 100%;
max-width: 560px;
margin: 0 auto;
aspect-ratio: 8 / 5;
}

.section-scene canvas {
display: block;
width: 100%;
height: 100%;
}
</style>

<script>
import { initSectionScene, type SceneName } from '../scripts/sectionScenes';

const canvases = Array.from(document.querySelectorAll<HTMLCanvasElement>('[data-section-scene]'));

if (canvases.length) {
// Labels are drawn to 2D canvases, so wait for the webfont rather than baking
// a fallback face into the textures.
const ready = document.fonts ? document.fonts.ready : Promise.resolve();
ready.then(() => {
canvases.forEach((canvas) => {
const section = canvas.closest('.reveal-section') ?? canvas;
try {
initSectionScene(canvas, canvas.dataset.sectionScene as SceneName, section);
} catch (error) {
// The section's text carries the message on its own, so fail quietly.
console.warn('Section scene unavailable:', error);
}
});
});
}
</script>
Loading
Loading