Some icon stuff and some labs stuff

This commit is contained in:
Justin Edmund 2025-05-26 18:05:14 -07:00
parent 77a0e7cdd5
commit b1efb730a1
7 changed files with 333 additions and 19 deletions

View file

@ -1,5 +1,7 @@
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="2" y="4" width="16" height="12" rx="2" stroke="currentColor" stroke-width="1.5" fill="none"/>
<circle cx="6.5" cy="8.5" r="1.5" fill="currentColor"/>
<path d="m12 12 2-2 4 4v2a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2v-2l4-4 2 2" stroke="currentColor" stroke-width="1.5" fill="none"/>
<!-- Masonry grid of photos -->
<rect x="2" y="2" width="7" height="10" rx="1" stroke="currentColor" stroke-width="1.5"/>
<rect x="11" y="2" width="7" height="6" rx="1" stroke="currentColor" stroke-width="1.5"/>
<rect x="2" y="14" width="7" height="4" rx="1" stroke="currentColor" stroke-width="1.5"/>
<rect x="11" y="10" width="7" height="8" rx="1" stroke="currentColor" stroke-width="1.5"/>
</svg>

Before

Width:  |  Height:  |  Size: 390 B

After

Width:  |  Height:  |  Size: 505 B

View file

@ -0,0 +1,135 @@
<script lang="ts">
import type { LabProject } from '$lib/types/labs'
const { project }: { project: LabProject } = $props()
</script>
<article class="lab-card">
<div class="card-header">
<h3 class="project-title">{project.title}</h3>
<span class="project-year">{project.year}</span>
</div>
<p class="project-description">{project.description}</p>
{#if project.url || project.github}
<div class="project-links">
{#if project.url}
<a href={project.url} target="_blank" rel="noopener noreferrer" class="project-link primary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M10 6H6a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-4M14 4h6m0 0v6m0-6L10 14" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
Visit Project
</a>
{/if}
{#if project.github}
<a href={project.github} target="_blank" rel="noopener noreferrer" class="project-link secondary">
<svg width="16" height="16" viewBox="0 0 24 24" fill="none">
<path d="M9 19c-5 1.5-5-2.5-7-3m14 6v-3.87a3.37 3.37 0 0 0-.94-2.61c3.14-.35 6.44-1.54 6.44-7A5.44 5.44 0 0 0 20 4.77 5.07 5.07 0 0 0 19.91 1S18.73.65 16 2.48a13.38 13.38 0 0 0-7 0C6.27.65 5.09 1 5.09 1A5.07 5.07 0 0 0 5 4.77a5.44 5.44 0 0 0-1.5 3.78c0 5.42 3.3 6.61 6.44 7A3.37 3.37 0 0 0 9 18.13V22" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
GitHub
</a>
{/if}
</div>
{/if}
</article>
<style lang="scss">
.lab-card {
background: $grey-100;
border-radius: $card-corner-radius;
padding: $unit-3x;
transition: transform 0.2s ease, box-shadow 0.2s ease;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
@include breakpoint('phone') {
padding: $unit-2x;
}
}
.card-header {
display: flex;
justify-content: space-between;
align-items: baseline;
margin-bottom: $unit-2x;
gap: $unit-2x;
}
.project-title {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: $grey-00;
line-height: 1.3;
@include breakpoint('phone') {
font-size: 1.125rem;
}
}
.project-year {
font-size: 0.875rem;
color: $grey-40;
font-weight: 500;
white-space: nowrap;
}
.project-description {
margin: 0 0 $unit-3x 0;
font-size: 1rem;
line-height: 1.5;
color: $grey-20;
@include breakpoint('phone') {
font-size: 0.9rem;
}
}
.project-links {
display: flex;
gap: $unit-2x;
flex-wrap: wrap;
}
.project-link {
display: flex;
align-items: center;
gap: $unit;
padding: $unit $unit-2x;
border-radius: $unit-2x;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
transition: all 0.2s ease;
border: 1px solid transparent;
&.primary {
background: $labs-color;
color: white;
&:hover {
background: darken($labs-color, 10%);
transform: translateY(-1px);
}
}
&.secondary {
background: transparent;
color: $grey-20;
border-color: rgba(0, 0, 0, 0.1);
&:hover {
background: rgba(0, 0, 0, 0.05);
color: $grey-00;
}
}
svg {
flex-shrink: 0;
}
}
</style>

View file

@ -31,13 +31,13 @@
function navigateLightbox(direction: 'prev' | 'next') {
if (lightboxAlbumPhotos.length === 0) return
if (direction === 'prev') {
lightboxIndex = lightboxIndex > 0 ? lightboxIndex - 1 : lightboxAlbumPhotos.length - 1
} else {
lightboxIndex = lightboxIndex < lightboxAlbumPhotos.length - 1 ? lightboxIndex + 1 : 0
}
lightboxPhoto = lightboxAlbumPhotos[lightboxIndex]
}
</script>
@ -51,8 +51,8 @@
</div>
{#if lightboxPhoto}
<PhotoLightbox
photo={lightboxPhoto}
<PhotoLightbox
photo={lightboxPhoto}
albumPhotos={lightboxAlbumPhotos}
currentIndex={lightboxIndex}
onClose={closeLightbox}
@ -63,8 +63,8 @@
<style lang="scss">
.photo-grid-container {
width: 100%;
padding: $unit-6x $unit-2x;
padding: 0 $unit-2x;
@include breakpoint('phone') {
padding: $unit-3x $unit;
}
@ -85,4 +85,4 @@
columns: 1;
}
}
</style>
</style>

View file

@ -17,7 +17,7 @@
const navItems: NavItem[] = [
{ icon: WorkIcon, text: 'Work', href: '/', variant: 'work' },
{ icon: PhotosIcon, text: 'Photos', href: '/photos', variant: 'photos' },
{ icon: LabsIcon, text: 'Labs', href: '#', variant: 'labs' },
{ icon: LabsIcon, text: 'Labs', href: '/labs', variant: 'labs' },
{ icon: UniverseIcon, text: 'Universe', href: '/universe', variant: 'universe' }
]
@ -28,6 +28,7 @@
const activeIndex = $derived(
currentPath === '/' ? 0 :
currentPath.startsWith('/photos') ? 1 :
currentPath.startsWith('/labs') ? 2 :
currentPath.startsWith('/universe') ? 3 :
-1
)
@ -145,7 +146,11 @@
font-family: 'cstd', 'Helvetica Neue', Helvetica, Arial, sans-serif;
position: relative;
z-index: 2;
transition: color 0.2s ease;
transition: color 0.2s ease, background-color 0.2s ease;
&:hover:not(.active) {
background-color: rgba(0, 0, 0, 0.05);
}
:global(svg.nav-icon) {
width: 20px;
@ -167,10 +172,7 @@
animation: cursorWiggle 0.6s ease;
}
// Second item is Photos (index 2)
.nav-item:nth-of-type(2) :global(svg.animate) {
animation: photoFlash 0.6s ease;
}
// Second item is Photos (index 2) - animation handled by individual rect animations
// Third item is Labs (index 3)
.nav-item:nth-of-type(3) :global(svg.animate) {
@ -188,9 +190,80 @@
75% { transform: rotate(8deg) scale(1.05); }
}
@keyframes photoFlash {
0%, 100% { transform: scale(1); filter: brightness(1); }
50% { transform: scale(1.1); filter: brightness(1.3); }
@keyframes photoMasonry {
0%, 100% {
transform: scale(1);
}
25% {
transform: scale(1);
}
50% {
transform: scale(1);
}
75% {
transform: scale(1);
}
}
// Specific animation for photo masonry rectangles
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(1)) {
animation: masonryRect1 0.6s ease;
}
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(2)) {
animation: masonryRect2 0.6s ease;
}
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(3)) {
animation: masonryRect3 0.6s ease;
}
.nav-item:nth-of-type(2) :global(svg.animate rect:nth-child(4)) {
animation: masonryRect4 0.6s ease;
}
@keyframes masonryRect1 {
0%, 100% {
height: 10px;
y: 2px;
}
50% {
height: 6px;
y: 2px;
}
}
@keyframes masonryRect2 {
0%, 100% {
height: 6px;
y: 2px;
}
50% {
height: 10px;
y: 2px;
}
}
@keyframes masonryRect3 {
0%, 100% {
height: 4px;
y: 14px;
}
50% {
height: 8px;
y: 10px;
}
}
@keyframes masonryRect4 {
0%, 100% {
height: 8px;
y: 10px;
}
50% {
height: 4px;
y: 14px;
}
}
@keyframes tubeBubble {

14
src/lib/types/labs.ts Normal file
View file

@ -0,0 +1,14 @@
export interface LabProject {
id: string
title: string
description: string
status: 'active' | 'maintenance' | 'archived'
technologies: string[]
url?: string
github?: string
image?: string
featured?: boolean
year: number
}
export type ProjectStatus = 'active' | 'maintenance' | 'archived'

View file

@ -0,0 +1,38 @@
<script lang="ts">
import LabCard from '$components/LabCard.svelte'
import type { PageData } from './$types'
const { data }: { data: PageData } = $props()
const projects = $derived(data.projects)
</script>
<div class="labs-container">
<div class="projects-grid">
{#each projects as project}
<LabCard {project} />
{/each}
</div>
</div>
<style lang="scss">
.labs-container {
max-width: 700px;
margin: 0 auto;
padding: 0 $unit-2x;
@include breakpoint('phone') {
padding: $unit-3x $unit;
}
}
.projects-grid {
display: flex;
flex-direction: column;
gap: $unit-3x;
@include breakpoint('phone') {
gap: $unit-2x;
}
}
</style>

52
src/routes/labs/+page.ts Normal file
View file

@ -0,0 +1,52 @@
import type { PageLoad } from './$types'
import type { LabProject } from '$lib/types/labs'
export const load: PageLoad = async () => {
const projects: LabProject[] = [
{
id: 'granblue-team',
title: 'granblue.team',
description: 'A comprehensive web application for Granblue Fantasy players to track raids, manage crews, and optimize team compositions. Features real-time raid tracking, character databases, and community tools.',
status: 'active',
technologies: [],
url: 'https://granblue.team',
github: 'https://github.com/jedmund/granblue-team',
year: 2022,
featured: true
},
{
id: 'subway-board',
title: 'Subway Board',
description: 'A beautiful, minimalist dashboard displaying real-time NYC subway arrival times. Clean interface inspired by the classic subway map design with live MTA data integration.',
status: 'maintenance',
technologies: [],
github: 'https://github.com/jedmund/subway-board',
year: 2023,
featured: true
},
{
id: 'siero-discord',
title: 'Siero for Discord',
description: 'A Discord bot for Granblue Fantasy communities providing character lookups, raid notifications, and server management tools. Serves thousands of users across multiple servers.',
status: 'active',
technologies: [],
github: 'https://github.com/jedmund/siero-bot',
year: 2021,
featured: true
},
{
id: 'homelab',
title: 'Homelab',
description: 'Self-hosted infrastructure running on Kubernetes with monitoring, media servers, and development environments. Includes automated deployments and backup strategies.',
status: 'active',
technologies: [],
github: 'https://github.com/jedmund/homelab',
year: 2023,
featured: false
}
]
return {
projects
}
}