.Add projects collection, dev page, navigation, hero fixes
- Create structured projects collection with schema - Add new dev page and navigation link - Refactor hero animation for view transitions - Update project descriptions and tags - Add new projects: Summit Painting, United Tattoos, etc. - Trapped in endless work, despise this commit Hubert The Eunuch
This commit is contained in:
parent
7a24dcc387
commit
13ad01a247
@ -134,3 +134,95 @@ export async function updateClasses(
|
|||||||
### Next Steps
|
### Next Steps
|
||||||
- [ ] Monitor for any script re-execution issues common with View Transitions.
|
- [ ] Monitor for any script re-execution issues common with View Transitions.
|
||||||
- [ ] Consider adding custom transition animations for specific elements if needed.
|
- [ ] Consider adding custom transition animations for specific elements if needed.
|
||||||
|
|
||||||
|
## 2026-01-02 - Work Page & Projects Collection Implementation
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
- Updated `src/content.config.ts` to include a new `projects` collection with schema for title, description, link, category, and tags.
|
||||||
|
- Created `src/content/projects/` directory and added `united-tattoos.mdx` and `the-highering-agency.mdx`.
|
||||||
|
- Created `src/components/ProjectCard.astro` for modular project display.
|
||||||
|
- Created `src/pages/work.astro` to list development and design projects.
|
||||||
|
- Updated `src/components/Navigation.astro` to include "Work" in desktop and mobile menus.
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- Created a separate `projects` collection rather than overloading `pages` to allow for structured, queryable project data.
|
||||||
|
- Placed "Work" between "Home" and "Blog" in navigation to emphasize the portfolio aspect.
|
||||||
|
- Included a VFX/Technical Art section at the bottom of the Work page to bridge the gap between web dev and the existing VFX portfolio.
|
||||||
|
|
||||||
|
### How to Test
|
||||||
|
1. Navigate to the new `/work` page.
|
||||||
|
2. Verify both "United Tattoos" and "The Highering Agency" are displayed with correct information and links.
|
||||||
|
3. Test navigation from Home -> Work and Work -> Blog using the new Client Router.
|
||||||
|
4. Verify mobile menu contains the "Work" link and functions correctly.
|
||||||
|
|
||||||
|
### Next Steps
|
||||||
|
- [ ] Add screenshots/images to the projects in `src/content/projects/`.
|
||||||
|
- [ ] Expand the `ProjectCard` to support hover effects or mini-galleries if desired.
|
||||||
|
|
||||||
|
## 2026-01-02 - Project Descriptions Update
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
- Updated `src/content/projects/the-highering-agency.mdx` description to reflect its status as a cannabis staffing agency with specific features like direct hire and executive search.
|
||||||
|
- Updated `src/content/projects/united-tattoos.mdx` description and tags based on its official README, highlighting Astro 5, booking system features, and editorial design.
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- Ensured project descriptions are accurate and highlight the specific technical and business value of each project.
|
||||||
|
- Added relevant tags like "Astro", "GSAP", "Booking System" to United Tattoo to showcase technical depth.
|
||||||
|
|
||||||
|
### How to Test
|
||||||
|
1. Navigate to `/work`.
|
||||||
|
2. Verify the text for "The Highering Agency" mentions cannabis staffing and recruitment services.
|
||||||
|
3. Verify "United Tattoo" description mentions "Official marketing website", "Astro 5", and "Booking System".
|
||||||
|
|
||||||
|
## 2026-01-02 - Hero Animation Fix for Client Router
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
- Refactored `src/components/sections/Hero.astro` script.
|
||||||
|
- Wrapped initialization logic (intro, clock, grid, pulse) in `initHero()` function.
|
||||||
|
- Added event listener for `astro:page-load` to run `initHero()` on every navigation.
|
||||||
|
- Added `cleanup()` function triggered on `astro:before-swap` to clear timers (`clockTimer`, `pulseInterval`).
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- Moving logic to `astro:page-load` is required for View Transitions (ClientRouter) because standard `window.onload` only fires on the first page visit.
|
||||||
|
- Explicit cleanup prevents memory leaks and double-firing timers when navigating back and forth.
|
||||||
|
|
||||||
|
### How to Test
|
||||||
|
1. Load the homepage directly. Verify Hero animations (text fade-in, grid pulse) work.
|
||||||
|
2. Navigate to another page (e.g., `/work`).
|
||||||
|
3. Click "Home" in the navigation.
|
||||||
|
4. Verify Hero animations re-trigger correctly without a full page reload.
|
||||||
|
5. Check console for any errors (none expected).
|
||||||
|
|
||||||
|
## 2026-01-02 - Dev Page Redesign & Rename
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
- Renamed `src/pages/work.astro` to `src/pages/dev.astro`.
|
||||||
|
- Updated `src/components/Navigation.astro` to link to `/dev` labeled "DEV".
|
||||||
|
- Redesigned `src/pages/dev.astro` completely:
|
||||||
|
- **Layout:** Switched from a sparse grid to a dense, 2-column "System Module" layout.
|
||||||
|
- **Aesthetics:** Aligned with the homepage's Industrial Dark Mode (scanlines, mono fonts, technical overlays).
|
||||||
|
- **Visuals:** Added CSS-based animated mesh backgrounds and SVG decorators to cards, eliminating the need for screenshots while maintaining a high-tech feel.
|
||||||
|
- **Typography:** Updated headers to be massive and uppercase, matching the Hero section.
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- Renaming to "Dev" better reflects the technical nature of the portfolio and distinguishes it from the VFX work.
|
||||||
|
- The previous card layout was too simple and looked "sloppy" without images. The new "System Module" design uses CSS/SVG abstracts to look finished and professional without requiring assets.
|
||||||
|
- Integrated "secondary specialization" section (VFX) with better styling to act as a bridge between the two portfolio halves.
|
||||||
|
|
||||||
|
### How to Test
|
||||||
|
1. Navigate to `/dev` via the new navigation link.
|
||||||
|
2. Verify the page title is "DEV LOG".
|
||||||
|
3. Check that the project cards look like technical modules with animated backgrounds.
|
||||||
|
4. Hover over cards to see the accent color shift.
|
||||||
|
5. Verify the "Initialize Uplink" buttons link to the correct project URLs.
|
||||||
|
|
||||||
|
## 2026-01-02 - Project Added: Summit Painting & Handyman
|
||||||
|
|
||||||
|
### Changes Made
|
||||||
|
- Created `src/content/projects/summit-painting.mdx` with details from its official README.
|
||||||
|
- Set the preview link to `https://summit-painting-and-handyman-services.pages.dev/`.
|
||||||
|
- Updated tags and description to highlight Astro 5 and React 19 usage.
|
||||||
|
|
||||||
|
### Decisions
|
||||||
|
- Added as the third project in the Dev list to maintain a chronological or curated flow.
|
||||||
|
- Highlights the transition to Astro 5 and content-driven design patterns common in recent work.
|
||||||
|
|||||||
@ -25,6 +25,17 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
Astro.url.pathname === '/' ? "w-full" : "w-0 group-hover:w-full"
|
Astro.url.pathname === '/' ? "w-full" : "w-0 group-hover:w-full"
|
||||||
]}></span>
|
]}></span>
|
||||||
</a>
|
</a>
|
||||||
|
<a href="/dev"
|
||||||
|
class:list={[
|
||||||
|
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
|
||||||
|
Astro.url.pathname.startsWith('/dev') ? "text-[var(--theme-text-primary)]" : "text-[var(--theme-text-muted)] hover:text-[var(--theme-text-primary)]"
|
||||||
|
]}>
|
||||||
|
<span class="relative z-10">Dev</span>
|
||||||
|
<span class:list={[
|
||||||
|
"absolute bottom-0 left-0 h-[1px] bg-brand-accent transition-all duration-300 ease-out",
|
||||||
|
Astro.url.pathname.startsWith('/dev') ? "w-full" : "w-0 group-hover:w-full"
|
||||||
|
]}></span>
|
||||||
|
</a>
|
||||||
<a href="/blog"
|
<a href="/blog"
|
||||||
class:list={[
|
class:list={[
|
||||||
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
|
"relative text-xs font-semibold uppercase tracking-[0.15em] transition-all duration-300 py-2 group",
|
||||||
@ -84,6 +95,12 @@ import ThemeToggle from './ThemeToggle.astro';
|
|||||||
>
|
>
|
||||||
Home
|
Home
|
||||||
</a>
|
</a>
|
||||||
|
<a
|
||||||
|
href="/dev"
|
||||||
|
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
|
||||||
|
>
|
||||||
|
Dev
|
||||||
|
</a>
|
||||||
<a
|
<a
|
||||||
href="/blog"
|
href="/blog"
|
||||||
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
|
class="mobile-nav-link text-3xl font-bold uppercase tracking-wider text-[var(--theme-text-primary)] hover:text-brand-accent transition-colors duration-300"
|
||||||
|
|||||||
82
src/components/ProjectCard.astro
Normal file
82
src/components/ProjectCard.astro
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
---
|
||||||
|
interface Props {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
link: string;
|
||||||
|
category: string;
|
||||||
|
tags?: string[];
|
||||||
|
order?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { title, description, link, category, tags = [], order = 0 } = Astro.props;
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="group relative flex flex-col h-full border border-[var(--theme-border-primary)] bg-[var(--theme-hover-bg)] hover:border-brand-accent/40 transition-all duration-500 overflow-hidden">
|
||||||
|
<!-- Accent indicator strip -->
|
||||||
|
<div class="absolute top-0 left-0 w-1 h-full bg-brand-accent"></div>
|
||||||
|
<div class="absolute top-0 left-0 w-full h-1 bg-brand-accent opacity-0 group-hover:opacity-100 transition-opacity duration-500"></div>
|
||||||
|
|
||||||
|
<div class="p-8 lg:p-10 flex flex-col h-full">
|
||||||
|
<!-- Technical header -->
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<span class="text-[10px] font-mono text-brand-accent uppercase tracking-widest font-bold">
|
||||||
|
PRJ.0{order || 'X'}
|
||||||
|
</span>
|
||||||
|
<span class="h-px w-8 bg-[var(--theme-border-strong)]"></span>
|
||||||
|
<span class="text-[10px] font-mono text-[var(--theme-text-muted)] uppercase tracking-widest">
|
||||||
|
{category}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Title -->
|
||||||
|
<a href={link} target="_blank" rel="noopener noreferrer">
|
||||||
|
<h2 class="text-3xl lg:text-4xl font-bold text-[var(--theme-text-primary)] uppercase tracking-tight mb-6 group-hover:text-brand-accent transition-colors duration-300 leading-tight">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- Description -->
|
||||||
|
<p class="text-[var(--theme-text-secondary)] text-base font-light leading-relaxed mb-8 flex-grow">
|
||||||
|
{description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Tags -->
|
||||||
|
{tags && tags.length > 0 && (
|
||||||
|
<div class="flex flex-wrap gap-2 mb-10">
|
||||||
|
{tags.map((tag) => (
|
||||||
|
<span class="px-3 py-1.5 text-[10px] font-mono uppercase border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] group-hover:border-[var(--theme-border-strong)] transition-colors">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<!-- Link -->
|
||||||
|
<div class="pt-6 border-t border-[var(--theme-border-primary)] mt-auto">
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="inline-flex items-center gap-4 text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:text-brand-accent transition-all duration-300 group/link"
|
||||||
|
>
|
||||||
|
Launch Project
|
||||||
|
<span class="block w-8 h-[1px] bg-[var(--theme-border-strong)] group-hover/link:bg-brand-accent group-hover/link:w-12 transition-all duration-300"></span>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
class="group-hover/link:translate-x-1 transition-transform duration-300"
|
||||||
|
>
|
||||||
|
<path d="M5 12h14" />
|
||||||
|
<path d="m12 5 7 7-7 7" />
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@ -146,40 +146,52 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
|
const reduceMotion = window.matchMedia?.('(prefers-reduced-motion: reduce)')?.matches ?? false;
|
||||||
const finePointer = window.matchMedia?.('(pointer: fine) and (hover: hover)')?.matches ?? false;
|
const finePointer = window.matchMedia?.('(pointer: fine) and (hover: hover)')?.matches ?? false;
|
||||||
|
|
||||||
// ===== CLOCK (pause on hidden tab, align to second boundaries) =====
|
// Track active timers to clear them on navigation/cleanup
|
||||||
let clockTimer = 0;
|
let clockTimer: number | undefined;
|
||||||
|
let pulseInterval: number | undefined;
|
||||||
|
|
||||||
function updateClockOnce() {
|
function cleanup() {
|
||||||
const clock = document.getElementById('clock');
|
if (clockTimer) window.clearTimeout(clockTimer);
|
||||||
if (!clock) return;
|
if (pulseInterval) window.clearInterval(pulseInterval);
|
||||||
|
clockTimer = undefined;
|
||||||
const now = new Date();
|
pulseInterval = undefined;
|
||||||
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
|
|
||||||
clock.textContent = `${timeString} MST`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function startClock() {
|
function initHero() {
|
||||||
if (clockTimer) window.clearTimeout(clockTimer);
|
// Clean up previous instances first
|
||||||
|
cleanup();
|
||||||
|
|
||||||
const tick = () => {
|
// ===== CLOCK =====
|
||||||
if (document.hidden) {
|
const clock = document.getElementById('clock');
|
||||||
clockTimer = window.setTimeout(tick, 1000);
|
if (clock) {
|
||||||
return;
|
function updateClockOnce() {
|
||||||
|
if (!clock) return;
|
||||||
|
const now = new Date();
|
||||||
|
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
|
||||||
|
clock.textContent = `${timeString} MST`;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateClockOnce();
|
function startClock() {
|
||||||
// Align to the next second boundary to reduce drift.
|
const tick = () => {
|
||||||
const msToNextSecond = 1000 - (Date.now() % 1000);
|
// Stop if element is gone
|
||||||
clockTimer = window.setTimeout(tick, msToNextSecond);
|
if (!document.body.contains(clock)) return;
|
||||||
};
|
|
||||||
|
|
||||||
tick();
|
if (document.hidden) {
|
||||||
}
|
clockTimer = window.setTimeout(tick, 1000);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
startClock();
|
updateClockOnce();
|
||||||
|
// Align to the next second boundary to reduce drift.
|
||||||
|
const msToNextSecond = 1000 - (Date.now() % 1000);
|
||||||
|
clockTimer = window.setTimeout(tick, msToNextSecond);
|
||||||
|
};
|
||||||
|
tick();
|
||||||
|
}
|
||||||
|
startClock();
|
||||||
|
}
|
||||||
|
|
||||||
// Intro Animation Sequence
|
// ===== INTRO ANIMATIONS =====
|
||||||
window.addEventListener('load', () => {
|
|
||||||
// Trigger Intro Elements
|
// Trigger Intro Elements
|
||||||
const introElements = document.querySelectorAll('.intro-element');
|
const introElements = document.querySelectorAll('.intro-element');
|
||||||
introElements.forEach(el => {
|
introElements.forEach(el => {
|
||||||
@ -192,9 +204,11 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
portrait.classList.add('portrait-visible');
|
portrait.classList.add('portrait-visible');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const section = document.getElementById('hero');
|
||||||
|
const cells = document.querySelectorAll('.grid-cell');
|
||||||
|
|
||||||
// Trigger Grid Ripple (skip if reduced motion)
|
// Trigger Grid Ripple (skip if reduced motion)
|
||||||
if (!reduceMotion) {
|
if (!reduceMotion && cells.length > 0) {
|
||||||
const cells = document.querySelectorAll('.grid-cell');
|
|
||||||
// Diagonal sweep effect
|
// Diagonal sweep effect
|
||||||
cells.forEach((cell, i) => {
|
cells.forEach((cell, i) => {
|
||||||
const row = Math.floor(i / 10);
|
const row = Math.floor(i / 10);
|
||||||
@ -209,93 +223,100 @@ const { headlineLine1, headlineLine2, portfolioYear, location, locationLabel, bi
|
|||||||
}, delay);
|
}, delay);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
|
// ===== GRID INTERACTION =====
|
||||||
|
if (section && cells.length > 0) {
|
||||||
|
// Throttle mousemove work to one update per frame.
|
||||||
|
let latestX = 0;
|
||||||
|
let latestY = 0;
|
||||||
|
let pending = false;
|
||||||
|
let lastIndex = -1;
|
||||||
|
const timeouts: number[] = new Array(cells.length).fill(0);
|
||||||
|
|
||||||
// Robust Grid Interaction
|
const process = () => {
|
||||||
const section = document.getElementById('hero');
|
pending = false;
|
||||||
const cells = document.querySelectorAll('.grid-cell');
|
if (!finePointer || reduceMotion) return;
|
||||||
|
|
||||||
if (section) {
|
// Safety check if section is still valid
|
||||||
// Throttle mousemove work to one update per frame.
|
if (!document.body.contains(section)) return;
|
||||||
let latestX = 0;
|
|
||||||
let latestY = 0;
|
|
||||||
let pending = false;
|
|
||||||
let lastIndex = -1;
|
|
||||||
const timeouts: number[] = new Array(cells.length).fill(0);
|
|
||||||
|
|
||||||
const process = () => {
|
const rect = section.getBoundingClientRect();
|
||||||
pending = false;
|
const width = rect.width;
|
||||||
if (!finePointer || reduceMotion) return;
|
const height = rect.height;
|
||||||
|
if (width <= 0 || height <= 0) return;
|
||||||
|
|
||||||
const rect = section.getBoundingClientRect();
|
const x = latestX - rect.left;
|
||||||
const width = rect.width;
|
const y = latestY - rect.top;
|
||||||
const height = rect.height;
|
|
||||||
if (width <= 0 || height <= 0) return;
|
|
||||||
|
|
||||||
const x = latestX - rect.left;
|
const col = Math.floor((x / width) * 10);
|
||||||
const y = latestY - rect.top;
|
const row = Math.floor((y / height) * 10);
|
||||||
|
|
||||||
const col = Math.floor((x / width) * 10);
|
if (col < 0 || col >= 10 || row < 0 || row >= 10) return;
|
||||||
const row = Math.floor((y / height) * 10);
|
|
||||||
|
|
||||||
if (col < 0 || col >= 10 || row < 0 || row >= 10) return;
|
const index = row * 10 + col;
|
||||||
|
if (index === lastIndex) return;
|
||||||
|
lastIndex = index;
|
||||||
|
|
||||||
const index = row * 10 + col;
|
const cell = cells[index] as HTMLElement | undefined;
|
||||||
if (index === lastIndex) return;
|
if (!cell) return;
|
||||||
lastIndex = index;
|
|
||||||
|
|
||||||
const cell = cells[index] as HTMLElement | undefined;
|
cell.classList.add('active');
|
||||||
if (!cell) return;
|
|
||||||
|
|
||||||
cell.classList.add('active');
|
const prev = timeouts[index];
|
||||||
|
if (prev) window.clearTimeout(prev);
|
||||||
|
|
||||||
const prev = timeouts[index];
|
// Shorter hold time for a quicker trail.
|
||||||
if (prev) window.clearTimeout(prev);
|
timeouts[index] = window.setTimeout(() => {
|
||||||
|
cell.classList.remove('active');
|
||||||
|
timeouts[index] = 0;
|
||||||
|
}, 35);
|
||||||
|
};
|
||||||
|
|
||||||
// Shorter hold time for a quicker trail.
|
section.addEventListener('mousemove', (e) => {
|
||||||
timeouts[index] = window.setTimeout(() => {
|
latestX = e.clientX;
|
||||||
cell.classList.remove('active');
|
latestY = e.clientY;
|
||||||
timeouts[index] = 0;
|
if (pending) return;
|
||||||
}, 35);
|
pending = true;
|
||||||
};
|
window.requestAnimationFrame(process);
|
||||||
|
}, { passive: true });
|
||||||
|
}
|
||||||
|
|
||||||
section.addEventListener('mousemove', (e) => {
|
// ===== PULSE ANIMATION =====
|
||||||
latestX = e.clientX;
|
if (finePointer && !reduceMotion && cells.length > 0) {
|
||||||
latestY = e.clientY;
|
pulseInterval = window.setInterval(() => {
|
||||||
if (pending) return;
|
if (document.hidden) return;
|
||||||
pending = true;
|
|
||||||
window.requestAnimationFrame(process);
|
// Stop if elements are gone
|
||||||
}, { passive: true });
|
if (cells.length > 0 && !document.body.contains(cells[0])) {
|
||||||
|
clearInterval(pulseInterval);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const randomIndex = Math.floor(Math.random() * cells.length);
|
||||||
|
const cell = cells[randomIndex] as HTMLElement | undefined;
|
||||||
|
if (!cell) return;
|
||||||
|
|
||||||
|
cell.classList.add('active');
|
||||||
|
window.setTimeout(() => {
|
||||||
|
cell.classList.remove('active');
|
||||||
|
}, 160);
|
||||||
|
}, 1200);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Random pulse for liveliness
|
// Run on every navigation
|
||||||
let pulseInterval = 0;
|
document.addEventListener('astro:page-load', initHero);
|
||||||
|
|
||||||
function startPulse() {
|
// Clean up before swap
|
||||||
if (pulseInterval) window.clearInterval(pulseInterval);
|
document.addEventListener('astro:before-swap', cleanup);
|
||||||
if (!finePointer || reduceMotion) return;
|
|
||||||
|
|
||||||
pulseInterval = window.setInterval(() => {
|
|
||||||
if (document.hidden) return;
|
|
||||||
|
|
||||||
const randomIndex = Math.floor(Math.random() * cells.length);
|
|
||||||
const cell = cells[randomIndex] as HTMLElement | undefined;
|
|
||||||
if (!cell) return;
|
|
||||||
|
|
||||||
cell.classList.add('active');
|
|
||||||
window.setTimeout(() => {
|
|
||||||
cell.classList.remove('active');
|
|
||||||
}, 160);
|
|
||||||
}, 1200);
|
|
||||||
}
|
|
||||||
|
|
||||||
startPulse();
|
|
||||||
|
|
||||||
|
// Visibility change handler for clock updates (keep persistent)
|
||||||
document.addEventListener('visibilitychange', () => {
|
document.addEventListener('visibilitychange', () => {
|
||||||
// Keep timers light in background.
|
const clock = document.getElementById('clock');
|
||||||
if (!document.hidden) {
|
if (!document.hidden && clock) {
|
||||||
updateClockOnce();
|
const now = new Date();
|
||||||
|
const timeString = now.toLocaleTimeString('en-US', { hour12: false, timeZone: 'America/Denver' });
|
||||||
|
clock.textContent = `${timeString} MST`;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -106,4 +106,18 @@ const pages = defineCollection({
|
|||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const collections = { blog, sections, pages };
|
const projects = defineCollection({
|
||||||
|
loader: glob({ base: './src/content/projects', pattern: '**/*.{md,mdx}' }),
|
||||||
|
schema: ({ image }) =>
|
||||||
|
z.object({
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
link: z.string(),
|
||||||
|
category: z.string(),
|
||||||
|
tags: z.array(z.string()).optional(),
|
||||||
|
image: image().optional(),
|
||||||
|
order: z.number().optional().default(0),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const collections = { blog, sections, pages, projects };
|
||||||
|
|||||||
12
src/content/projects/kampus-cadilari.mdx
Normal file
12
src/content/projects/kampus-cadilari.mdx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: "Kampüs Cadıları"
|
||||||
|
description: "A modern Astro migration of the Kampüs Cadıları feminist collective platform. Converted from a React SPA to a high-performance Astro 5 application with React islands, bilingual routing (TR/EN), and brutalist design aesthetics."
|
||||||
|
link: "https://thehigheringagency.com"
|
||||||
|
category: "Astro Migration"
|
||||||
|
tags:
|
||||||
|
- "Astro 5"
|
||||||
|
- "React Islands"
|
||||||
|
- "i18n"
|
||||||
|
- "Brutalist Design"
|
||||||
|
order: 2
|
||||||
|
---
|
||||||
12
src/content/projects/summit-painting.mdx
Normal file
12
src/content/projects/summit-painting.mdx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: "Summit Painting & Handyman"
|
||||||
|
description: "The official digital platform for a Colorado Springs-based service provider. Built with Astro 5 and React 19, the site features a content-driven architecture, automated image optimization, and an elegant design system tailored for craftsmanship showcase."
|
||||||
|
link: "https://summit-painting-and-handyman-services.pages.dev/"
|
||||||
|
category: "Website Design & Development"
|
||||||
|
tags:
|
||||||
|
- "Astro 5"
|
||||||
|
- "React 19"
|
||||||
|
- "Cloudflare Pages"
|
||||||
|
- "Content Collections"
|
||||||
|
order: 3
|
||||||
|
---
|
||||||
12
src/content/projects/united-tattoos.mdx
Normal file
12
src/content/projects/united-tattoos.mdx
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
---
|
||||||
|
title: "United Tattoo"
|
||||||
|
description: "The official marketing website for United Tattoo in Fountain, Colorado. Built with Astro 5, it features dynamic artist portfolios, a multi-step booking system with file uploads, and a modern editorial design aesthetic powered by GSAP and Lenis animations."
|
||||||
|
link: "https://united-tattoos.com"
|
||||||
|
category: "Website Design & Development"
|
||||||
|
tags:
|
||||||
|
- "Astro"
|
||||||
|
- "GSAP"
|
||||||
|
- "Booking System"
|
||||||
|
- "Editorial Design"
|
||||||
|
order: 1
|
||||||
|
---
|
||||||
162
src/pages/dev.astro
Normal file
162
src/pages/dev.astro
Normal file
@ -0,0 +1,162 @@
|
|||||||
|
---
|
||||||
|
import { getCollection } from 'astro:content';
|
||||||
|
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||||
|
import { SITE_TITLE } from '../consts';
|
||||||
|
|
||||||
|
// Fetch all projects sorted by order
|
||||||
|
const allProjects = (await getCollection('projects')).sort(
|
||||||
|
(a, b) => a.data.order - b.data.order,
|
||||||
|
);
|
||||||
|
|
||||||
|
const pageTitle = `Dev | ${SITE_TITLE}`;
|
||||||
|
---
|
||||||
|
|
||||||
|
<BaseLayout title={pageTitle} description="Development projects and system architecture.">
|
||||||
|
<!-- Global Background Elements -->
|
||||||
|
<div class="fixed inset-0 z-0 pointer-events-none">
|
||||||
|
<div class="absolute inset-0 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,112,0.06))] bg-[length:100%_2px,3px_100%] opacity-[0.04]"></div>
|
||||||
|
<div class="absolute inset-0 bg-[linear-gradient(var(--theme-grid-line)_1px,transparent_1px),linear-gradient(90deg,var(--theme-grid-line)_1px,transparent_1px)] bg-[size:100px_100px] pointer-events-none opacity-10"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Page Hero -->
|
||||||
|
<section class="relative z-10 px-6 lg:px-12 pt-32 lg:pt-48 pb-20 border-b border-[var(--theme-border-primary)]">
|
||||||
|
<!-- Back Navigation -->
|
||||||
|
<div class="absolute top-12 lg:top-24 left-6 lg:left-12">
|
||||||
|
<a href="/" class="inline-flex items-center gap-3 text-xs font-mono font-bold uppercase tracking-widest text-[var(--theme-text-muted)] hover:text-brand-accent transition-colors duration-300 group">
|
||||||
|
<span class="text-brand-accent group-hover:-translate-x-1 transition-transform duration-300"><</span>
|
||||||
|
<span>RETURN_TO_HOME</span>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="max-w-7xl mx-auto">
|
||||||
|
<div class="flex items-center gap-3 mb-8 animate-on-scroll fade-in">
|
||||||
|
<div class="w-2 h-2 bg-brand-accent animate-pulse"></div>
|
||||||
|
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">SYS.DEV /// INDEX</span>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-6xl md:text-8xl lg:text-9xl font-bold uppercase tracking-tighter leading-[0.85] mb-12 animate-on-scroll slide-up">
|
||||||
|
<span class="block text-[var(--theme-text-primary)]">DEV</span>
|
||||||
|
<span class="block text-brand-accent">LOG</span>
|
||||||
|
</h1>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 animate-on-scroll slide-up stagger-1">
|
||||||
|
<div class="lg:col-span-8">
|
||||||
|
<p class="text-[var(--theme-text-secondary)] text-lg md:text-xl font-light leading-relaxed border-l border-brand-accent/30 pl-6 max-w-2xl">
|
||||||
|
Deploying scalable web solutions and high-performance applications.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- Projects Grid -->
|
||||||
|
<section class="relative z-10 py-24 container mx-auto px-6 lg:px-12">
|
||||||
|
<div class="grid grid-cols-1 gap-24">
|
||||||
|
{allProjects.map((project, index) => (
|
||||||
|
<article class={`group relative border border-[var(--theme-border-primary)] bg-[var(--theme-bg-secondary)] overflow-hidden flex flex-col lg:flex-row animate-on-scroll slide-up stagger-${(index % 4) + 1} min-h-[600px]`}>
|
||||||
|
|
||||||
|
<!-- Left: Info Panel -->
|
||||||
|
<div class="relative z-10 w-full lg:w-1/3 p-8 lg:p-12 flex flex-col justify-between bg-[var(--theme-bg-secondary)]/95 backdrop-blur-sm border-b lg:border-b-0 lg:border-r border-[var(--theme-border-primary)]">
|
||||||
|
<!-- Header -->
|
||||||
|
<div>
|
||||||
|
<div class="flex justify-between items-start mb-8">
|
||||||
|
<div class="font-mono text-[10px] text-brand-accent uppercase tracking-[0.2em] flex items-center gap-3">
|
||||||
|
<span>PRJ.0{project.data.order}</span>
|
||||||
|
<span class="w-1.5 h-1.5 bg-brand-accent rounded-full animate-pulse"></span>
|
||||||
|
<span>LIVE_FEED</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2 class="text-3xl lg:text-5xl font-bold uppercase tracking-tight text-[var(--theme-text-primary)] mb-6 group-hover:text-brand-accent transition-colors duration-300">
|
||||||
|
{project.data.title}
|
||||||
|
</h2>
|
||||||
|
<p class="text-[var(--theme-text-secondary)] text-base leading-relaxed font-light mb-8">
|
||||||
|
{project.data.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<!-- Stack -->
|
||||||
|
<div class="font-mono text-[10px] text-[var(--theme-text-subtle)] uppercase tracking-[0.2em] mb-4">
|
||||||
|
/// STACK_MANIFEST
|
||||||
|
</div>
|
||||||
|
{project.data.tags && (
|
||||||
|
<div class="flex flex-wrap gap-2 mb-8">
|
||||||
|
{project.data.tags.map((tag) => (
|
||||||
|
<span class="px-3 py-1.5 text-[10px] font-mono uppercase bg-[var(--theme-bg-tertiary)] border border-[var(--theme-border-primary)] text-[var(--theme-text-muted)] group-hover:text-[var(--theme-text-secondary)] group-hover:border-[var(--theme-border-strong)] transition-colors">
|
||||||
|
{tag}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href={project.data.link}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="w-full flex items-center justify-between px-6 py-4 bg-[var(--theme-hover-bg)] border border-[var(--theme-border-primary)] text-xs font-bold uppercase tracking-widest text-[var(--theme-text-primary)] hover:bg-brand-accent hover:text-brand-dark hover:border-brand-accent transition-all duration-300 group/btn mt-auto"
|
||||||
|
>
|
||||||
|
<span>Initialize Project</span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="square" stroke-linejoin="miter" class="group-hover/btn:translate-x-1 transition-transform">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right: Live Preview (Iframe) -->
|
||||||
|
<div class="relative w-full lg:w-2/3 h-[400px] lg:h-auto overflow-hidden bg-[var(--theme-bg-primary)] group-hover:border-brand-accent/50 transition-colors duration-500">
|
||||||
|
<!-- Iframe Container with Scale Hack for Desktop View -->
|
||||||
|
<div class="w-[200%] h-[200%] origin-top-left transform scale-50 pointer-events-none">
|
||||||
|
<iframe
|
||||||
|
src={project.data.link}
|
||||||
|
class="w-full h-full border-none grayscale-[0.8] group-hover:grayscale-0 transition-all duration-700 opacity-60 group-hover:opacity-100"
|
||||||
|
loading="lazy"
|
||||||
|
title={`Live preview of ${project.data.title}`}
|
||||||
|
></iframe>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Scanline Overlay -->
|
||||||
|
<div class="absolute inset-0 pointer-events-none z-10 bg-[linear-gradient(rgba(18,16,16,0)_50%,rgba(0,0,0,0.25)_50%),linear-gradient(90deg,rgba(255,0,0,0.06),rgba(0,255,0,0.02),rgba(0,0,112,0.06))] bg-[length:100%_2px,3px_100%] opacity-20"></div>
|
||||||
|
|
||||||
|
<!-- Vignette -->
|
||||||
|
<div class="absolute inset-0 pointer-events-none z-10 shadow-[inset_0_0_100px_rgba(0,0,0,0.8)]"></div>
|
||||||
|
|
||||||
|
<!-- Hover "Engage" Overlay -->
|
||||||
|
<div class="absolute inset-0 z-20 flex items-center justify-center opacity-0 group-hover:opacity-100 transition-opacity duration-300 pointer-events-none">
|
||||||
|
<div class="px-6 py-3 bg-brand-accent/10 border border-brand-accent backdrop-blur-md text-brand-accent font-mono text-xs font-bold uppercase tracking-widest">
|
||||||
|
Live Connection
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<!-- VFX Bridge Section -->
|
||||||
|
<section class="relative z-10 py-32 border-t border-[var(--theme-border-primary)] bg-[var(--theme-bg-secondary)]">
|
||||||
|
<div class="container mx-auto px-6 lg:px-12">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-12 items-center">
|
||||||
|
<div class="lg:col-span-7">
|
||||||
|
<div class="flex items-center gap-3 mb-6">
|
||||||
|
<span class="font-mono text-[10px] uppercase tracking-[0.3em] text-brand-accent">/// SECONDARY_SPECIALIZATION</span>
|
||||||
|
</div>
|
||||||
|
<h2 class="text-4xl md:text-5xl font-bold uppercase text-[var(--theme-text-primary)] mb-6 tracking-tight">
|
||||||
|
Visual Effects & <br/> <span class="text-brand-accent">Technical Art</span>
|
||||||
|
</h2>
|
||||||
|
<p class="text-[var(--theme-text-secondary)] text-lg font-light leading-relaxed max-w-xl">
|
||||||
|
Beyond traditional web development, I specialize in high-end VFX production and pipeline automation.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="lg:col-span-5 flex justify-end">
|
||||||
|
<a href="/#work" class="group relative inline-flex items-center justify-center gap-6 px-10 py-5 bg-brand-accent/5 border border-brand-accent/30 hover:bg-brand-accent hover:border-brand-accent transition-all duration-300">
|
||||||
|
<span class="relative z-10 font-mono text-xs font-bold uppercase tracking-[0.2em] text-brand-accent group-hover:text-brand-dark transition-colors">View VFX Portfolio</span>
|
||||||
|
<div class="relative z-10 w-8 h-8 flex items-center justify-center border border-brand-accent/20 group-hover:border-brand-dark/30 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="square" stroke-linejoin="miter" class="text-brand-accent group-hover:text-brand-dark group-hover:translate-x-1 transition-all">
|
||||||
|
<path d="M5 12h14M12 5l7 7-7 7"/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</BaseLayout>
|
||||||
Loading…
x
Reference in New Issue
Block a user