Cayden Website 2.0: Rewrite with t3code & hand
This commit is contained in:
parent
75a0e8f5fc
commit
243527af9f
34 changed files with 1652 additions and 1001 deletions
|
|
@ -6,13 +6,13 @@
|
||||||
# Note that environment variables can be set in several places
|
# Note that environment variables can be set in several places
|
||||||
# See https://docs.gitlab.com/ci/variables/#cicd-variable-precedence
|
# See https://docs.gitlab.com/ci/variables/#cicd-variable-precedence
|
||||||
stages:
|
stages:
|
||||||
- test
|
- test
|
||||||
- secret-detection
|
- secret-detection
|
||||||
sast:
|
sast:
|
||||||
stage: test
|
stage: test
|
||||||
include:
|
include:
|
||||||
- template: Security/SAST.gitlab-ci.yml
|
- template: Security/SAST.gitlab-ci.yml
|
||||||
- template: Security/Secret-Detection.gitlab-ci.yml
|
- template: Security/Secret-Detection.gitlab-ci.yml
|
||||||
variables:
|
variables:
|
||||||
SECRET_DETECTION_ENABLED: 'true'
|
SECRET_DETECTION_ENABLED: 'true'
|
||||||
secret_detection:
|
secret_detection:
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"tabWidth": 4,
|
"tabWidth": 4,
|
||||||
"singleQuote": true,
|
"singleQuote": true,
|
||||||
"useTabs": true
|
"useTabs": true,
|
||||||
|
"plugins": ["prettier-plugin-tailwindcss"],
|
||||||
|
"tailwindStylesheet": "./app/globals.css"
|
||||||
}
|
}
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
<!-- BEGIN:nextjs-agent-rules -->
|
<!-- BEGIN:nextjs-agent-rules -->
|
||||||
|
|
||||||
# This is NOT the Next.js you know
|
# This is NOT the Next.js you know
|
||||||
|
|
||||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||||
|
|
|
||||||
81
README.md
81
README.md
|
|
@ -1,80 +1,23 @@
|
||||||
# Cayden's Website
|
# Cayden's Website
|
||||||
|
|
||||||
A professional portfolio and markdown-powered blog built with **Next.js 16**, **React 19**, **TypeScript**, and **Tailwind CSS 4**.
|
A developer portfolio built with **Next.js 16**, **React 19**, **TypeScript**, and **Tailwind CSS 4**, featuring a full-page React Bits `GradientWaves` ("Aurora") background behind sharp, translucent `1px` panels.
|
||||||
|
|
||||||
This site serves as a digital portfolio for Cayden, a Web Developer & Focus-Driven Leader, highlighting expertise in Clinical UI/UX, EHR systems, and large-scale logistics. It features a custom Nord-inspired design and a dynamic blog system.
|
## Structure
|
||||||
|
|
||||||
## 🚀 Features
|
|
||||||
|
|
||||||
- **Modern Tech Stack**: Built with React 19 and Next.js 16 using the App Router.
|
|
||||||
- **Tailwind CSS 4**: Modern, high-performance styling with the Nord color palette.
|
|
||||||
- **Markdown Blog**: Fully dynamic blog system powered by `remark` and `gray-matter`.
|
|
||||||
- **Responsive Design**: Elegant mobile-first navigation and layout.
|
|
||||||
- **Optimized Typography**: Custom Google Fonts (Cormorant Garamond & Poppins) via `next/font`.
|
|
||||||
- **Type-Safe**: Full TypeScript implementation for robust development.
|
|
||||||
|
|
||||||
## 🛠️ Tech Stack
|
|
||||||
|
|
||||||
- **Framework**: [Next.js 16](https://nextjs.org/) (App Router)
|
|
||||||
- **Library**: [React 19](https://react.dev/)
|
|
||||||
- **Styling**: [Tailwind CSS 4](https://tailwindcss.com/)
|
|
||||||
- **Language**: [TypeScript](https://www.typescriptlang.org/)
|
|
||||||
- **Content**: Markdown with [Remark](https://github.com/remarkjs/remark) & [Gray-matter](https://github.com/jonschlinkert/gray-matter)
|
|
||||||
- **Icons**: [Heroicons](https://heroicons.com/)
|
|
||||||
- **Date Handling**: [Moment.js](https://momentjs.com/)
|
|
||||||
|
|
||||||
## 📂 Project Structure
|
|
||||||
|
|
||||||
```text
|
```text
|
||||||
├── app/ # Next.js App Router (pages, layouts, globals)
|
app/ Root layout, global styles, home page
|
||||||
│ ├── blog/ # Blog listing and dynamic article pages
|
components/
|
||||||
│ ├── contact/ # Contact page
|
layout/ Header, Footer, PageBackground
|
||||||
│ └── layout.tsx # Root layout with fonts and navigation
|
sections/ Hero, Projects, Feats, Contact
|
||||||
├── articles/ # Markdown content for blog posts
|
ui/ Reveal, SectionHeading
|
||||||
├── components/ # Reusable UI and Layout components
|
GradientWaves.tsx Vendored from React Bits
|
||||||
│ ├── layout/ # Header, Footer, etc.
|
lib/site.ts All editable content and types
|
||||||
│ └── ui/ # Generic UI elements
|
|
||||||
├── lib/ # Utility functions and data fetching (markdown parser)
|
|
||||||
├── public/ # Static assets (images, icons)
|
|
||||||
├── types/ # TypeScript interface definitions
|
|
||||||
└── package.json # Dependencies and scripts
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 🏁 Getting Started
|
## Development
|
||||||
|
|
||||||
First, install the dependencies:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
npm install
|
|
||||||
# or
|
|
||||||
bun install
|
bun install
|
||||||
|
bun dev # http://localhost:3000
|
||||||
|
bun run format
|
||||||
```
|
```
|
||||||
|
|
||||||
Then, run the development server:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
# or
|
|
||||||
bun dev
|
|
||||||
```
|
|
||||||
|
|
||||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
|
||||||
|
|
||||||
## 📝 Writing Articles
|
|
||||||
|
|
||||||
To add a new blog post, create a `.md` file in the `/articles` directory with the following frontmatter:
|
|
||||||
|
|
||||||
```markdown
|
|
||||||
---
|
|
||||||
title: "Your Title"
|
|
||||||
date: "YYYY-MM-DD"
|
|
||||||
category: "Category Name"
|
|
||||||
description: "Brief summary of the article"
|
|
||||||
---
|
|
||||||
|
|
||||||
Your content here...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📜 License
|
|
||||||
|
|
||||||
This project is private and intended for portfolio demonstration.
|
|
||||||
|
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
import Link from "next/link"
|
|
||||||
import {ArrowLeftIcon} from "@heroicons/react/24/solid"
|
|
||||||
import {getArticleData, getSortedArticles} from "@/lib/articles";
|
|
||||||
|
|
||||||
export async function generateStaticParams() {
|
|
||||||
const articles = getSortedArticles();
|
|
||||||
|
|
||||||
return articles.map((article) => ({
|
|
||||||
slug: article.id,
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
const Article = async ({params}: { params: Promise<{ slug: string }> }) => {
|
|
||||||
const {slug} = await params;
|
|
||||||
const articleData = await getArticleData(slug);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<section className={"mx-auto w-10/12 md:w-1/2 mt-20 flex flex-col gap-5"}>
|
|
||||||
<div className={"flex justify-between font-poppins"}>
|
|
||||||
<Link href={"/blog"} className={"flex flex-row gap-1 place-items-center"}>
|
|
||||||
<ArrowLeftIcon width={20}/>
|
|
||||||
<p>blog homepage</p>
|
|
||||||
</Link>
|
|
||||||
<p>{articleData.date.toString()}</p>
|
|
||||||
</div>
|
|
||||||
<article className={"article"} dangerouslySetInnerHTML={{__html: articleData.contentHtml}}/>
|
|
||||||
</section>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Article;
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import {getCategorizedArticles} from "@/lib/articles";
|
|
||||||
import ArticleItemList from "@/components/ArticleListItem";
|
|
||||||
|
|
||||||
export default async function BlogPage() {
|
|
||||||
const articles = getCategorizedArticles()
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<section className={"mx-auto w-11/12 md:w-1/2 mt-20 flex flex-col gap-16 mb-20"}>
|
|
||||||
<header className={"font-cormorantGaramond font-light text-6xl text-center"}>
|
|
||||||
<h1 className={'text-nord-14'}>Blog & thoughts</h1>
|
|
||||||
</header>
|
|
||||||
<section className={"md:grid md:grid-cols-2 flex flex-col gap-10"}>
|
|
||||||
{articles != null && Object.keys(articles).map((article) => (
|
|
||||||
<ArticleItemList category={article} articles={articles[article]} key={article}/>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
</section>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
export default async function Contact() {
|
|
||||||
return (
|
|
||||||
<div className="flex flex-col items-center justify-center min-h-screen py-2">
|
|
||||||
<h1 className="text-4xl font-bold">Contact page, wip.</h1>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
118
app/globals.css
118
app/globals.css
|
|
@ -1,84 +1,74 @@
|
||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--font-cormorantGaramond: "Cormorant_Garamond", "sans-serif";
|
--font-display:
|
||||||
--font-poppins: "Poppins", "sans-serif";
|
var(--font-space-grotesk), ui-sans-serif, system-ui, sans-serif;
|
||||||
|
--font-sans: var(--font-inter), ui-sans-serif, system-ui, sans-serif;
|
||||||
/* Nord: Polar Night */
|
--font-mono:
|
||||||
--color-nord-0: #2e3440;
|
var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, monospace;
|
||||||
--color-nord-1: #3b4252;
|
|
||||||
--color-nord-2: #4D535F;
|
|
||||||
--color-nord-3: #434c5e;
|
|
||||||
|
|
||||||
/* Nord: Snow Storm */
|
|
||||||
--color-nord-4: #d8dee9;
|
|
||||||
--color-nord-5: #e5e9f0;
|
|
||||||
--color-nord-6: #eceff4;
|
|
||||||
|
|
||||||
/* Nord: Frost */
|
|
||||||
--color-nord-7: #8fbcbb;
|
|
||||||
--color-nord-8: #88c0d0;
|
|
||||||
--color-nord-9: #81a1c1;
|
|
||||||
--color-nord-10: #5e81ac;
|
|
||||||
|
|
||||||
/* Nord: Aurora */
|
|
||||||
--color-nord-11: #bf616a;
|
|
||||||
--color-nord-12: #d08770;
|
|
||||||
--color-nord-13: #ebcb8b;
|
|
||||||
--color-nord-14: #a3be8c;
|
|
||||||
--color-nord-15: #b48ead;
|
|
||||||
|
|
||||||
/* Optional semantic aliases */
|
|
||||||
--color-polar-night-dark: #2e3440;
|
|
||||||
--color-polar-night: #3b4252;
|
|
||||||
--color-polar-night-light: #434c5e;
|
|
||||||
--color-polar-night-lighter: #4c566a;
|
|
||||||
|
|
||||||
--color-snow-storm-dark: #d8dee9;
|
|
||||||
--color-snow-storm: #e5e9f0;
|
|
||||||
--color-snow-storm-light: #eceff4;
|
|
||||||
|
|
||||||
--color-frost-green: #8fbcbb;
|
|
||||||
--color-frost-cyan: #88c0d0;
|
|
||||||
--color-frost-blue: #81a1c1;
|
|
||||||
--color-frost-deep-blue: #5e81ac;
|
|
||||||
|
|
||||||
--color-aurora-red: #bf616a;
|
|
||||||
--color-aurora-orange: #d08770;
|
|
||||||
--color-aurora-yellow: #ebcb8b;
|
|
||||||
--color-aurora-green: #a3be8c;
|
|
||||||
--color-aurora-purple: #b48ead;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.article {
|
@theme {
|
||||||
@apply flex flex-col gap-3 font-poppins text-lg tracking-wide mb-20
|
--color-obsidian: #040807;
|
||||||
|
--color-aurora: #34d399;
|
||||||
|
--color-crest: #d1fae5;
|
||||||
|
--color-ink: #f2faf6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > h1 {
|
@utility container-page {
|
||||||
@apply text-4xl font-cormorantGaramond text-center tracking-tight;
|
@apply mx-auto max-w-5xl px-6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > h2 {
|
@utility panel {
|
||||||
@apply text-3xl font-cormorantGaramond text-center tracking-tight;
|
@apply border border-white/12 bg-obsidian/55 backdrop-blur-md;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > ol {
|
@utility btn-primary {
|
||||||
@apply list-decimal flex flex-col mx-10 gap-2;
|
@apply border border-aurora bg-aurora px-5 py-2.5 text-obsidian transition-colors hover:border-crest hover:bg-crest;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > ul {
|
@utility btn-ghost {
|
||||||
@apply list-disc flex flex-col mx-10 gap-2;
|
@apply border border-white/20 bg-obsidian/45 px-5 py-2.5 text-ink backdrop-blur-md transition-colors hover:border-aurora hover:text-aurora;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > pre {
|
html {
|
||||||
@apply bg-neutral-950 text-neutral-50 p-5 overflow-scroll;
|
scroll-behavior: smooth;
|
||||||
|
scroll-padding-top: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > blockquote {
|
::selection {
|
||||||
@apply pl-5 italic border-l-2 border-neutral-600;
|
background: var(--color-aurora);
|
||||||
|
color: var(--color-obsidian);
|
||||||
}
|
}
|
||||||
|
|
||||||
.article > p > img {
|
:focus-visible {
|
||||||
@apply w-1/2 mx-auto;
|
outline: 1px solid var(--color-aurora);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (scripting: enabled) {
|
||||||
|
.reveal {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(10px);
|
||||||
|
transition:
|
||||||
|
opacity 0.5s cubic-bezier(0.16, 1, 0.3, 1),
|
||||||
|
transform 0.5s cubic-bezier(0.16, 1, 0.3, 1);
|
||||||
|
transition-delay: var(--reveal-delay, 0ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reveal[data-visible='true'] {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
html {
|
||||||
|
scroll-behavior: auto;
|
||||||
|
}
|
||||||
|
.reveal {
|
||||||
|
opacity: 1;
|
||||||
|
transform: none;
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,41 +1,52 @@
|
||||||
import React from "react";
|
import type { ReactNode } from 'react';
|
||||||
import type {Metadata} from "next";
|
import type { Metadata, Viewport } from 'next';
|
||||||
import {Cormorant_Garamond, Poppins} from "next/font/google";
|
import { Inter, JetBrains_Mono, Space_Grotesk } from 'next/font/google';
|
||||||
import "./globals.css";
|
import './globals.css';
|
||||||
import Header from "@/components/layout/layout-header";
|
import Header from '@/components/layout/layout-header';
|
||||||
|
import Footer from '@/components/layout/layout-footer';
|
||||||
|
import PageBackground from '@/components/layout/page-background';
|
||||||
|
|
||||||
const cormorantGaramond = Cormorant_Garamond({
|
const spaceGrotesk = Space_Grotesk({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
variable: "--font-cormorantGaramond",
|
variable: '--font-space-grotesk',
|
||||||
weight: ['400']
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const poppins = Poppins({
|
const inter = Inter({
|
||||||
subsets: ['latin'],
|
subsets: ['latin'],
|
||||||
variable: "--font-poppins",
|
variable: '--font-inter',
|
||||||
weight: ['400', '600']
|
});
|
||||||
})
|
|
||||||
|
|
||||||
|
const jetbrainsMono = JetBrains_Mono({
|
||||||
|
subsets: ['latin'],
|
||||||
|
variable: '--font-jetbrains-mono',
|
||||||
|
weight: ['400', '500'],
|
||||||
|
});
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "sh ~./caydencodes.sh",
|
title: 'Cayden — Web Developer',
|
||||||
description: "Come and see what I'm all about.",
|
description:
|
||||||
|
'Web developer building clinical interfaces and fleet software.',
|
||||||
};
|
};
|
||||||
|
|
||||||
export default async function RootLayout({
|
export const viewport: Viewport = {
|
||||||
|
themeColor: '#040807',
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
children,
|
children,
|
||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<html
|
<html
|
||||||
lang="en"
|
lang="en"
|
||||||
className={`${cormorantGaramond.variable} ${poppins.variable} bg-neutral-100 h-full antialiased`}
|
className={`${spaceGrotesk.variable} ${inter.variable} ${jetbrainsMono.variable} h-full bg-obsidian antialiased`}
|
||||||
>
|
>
|
||||||
<body
|
<body className="relative isolate flex min-h-full flex-col font-sans text-ink">
|
||||||
className="min-h-full flex flex-col bg-polar-night-lighter text-snow-storm text-shadow-snow-storm-dark">
|
<PageBackground />
|
||||||
<Header/>
|
<Header />
|
||||||
{children}
|
<div className="flex-1">{children}</div>
|
||||||
|
<Footer />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
98
app/page.tsx
98
app/page.tsx
|
|
@ -1,93 +1,15 @@
|
||||||
import React from "react";
|
import Hero from '@/components/sections/hero';
|
||||||
|
import Projects from '@/components/sections/projects';
|
||||||
|
import Feats from '@/components/sections/feats';
|
||||||
|
import Contact from '@/components/sections/contact';
|
||||||
|
|
||||||
export default async function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
<main className="mx-auto mb-20 mt-20 flex w-11/12 max-w-4xl flex-col gap-12 md:w-2/3">
|
<main>
|
||||||
<section className="flex flex-col items-center gap-8 text-center font-cormorantGaramond">
|
<Hero />
|
||||||
<img
|
<Projects />
|
||||||
src="/cayden-portrait.png"
|
<Feats />
|
||||||
alt="Portrait of Cayden"
|
<Contact />
|
||||||
className="h-40 w-40 rounded-full object-cover shadow-lg"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<header className="flex flex-col items-center gap-4">
|
|
||||||
<h1 className="text-5xl font-light leading-tight md:text-6xl">
|
|
||||||
Howdy, I'm Cayden.
|
|
||||||
</h1>
|
|
||||||
|
|
||||||
<p className="font-poppins text-2xl font-light leading-tight opacity-80">
|
|
||||||
Web Developer & Focus-Driven Leader
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-wrap items-center justify-center gap-4 font-poppins text-sm tracking-wider text-gray-600 sm:gap-6 sm:text-base md:gap-8">
|
|
||||||
{["JavaScript", "TypeScript", "Python", "Go", "React", "Next.js"].map((tech, index, arr) => (
|
|
||||||
<React.Fragment key={tech}>
|
|
||||||
<span className="cursor-default text-snow-storm font-medium transition-colors duration-200 hover:text-nord-12">
|
|
||||||
{tech}
|
|
||||||
</span>
|
|
||||||
{index < arr.length - 1 && (
|
|
||||||
<span className="select-none text-snow-storm">•</span>
|
|
||||||
)}
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="flex flex-col gap-6 font-cormorantGaramond text-lg leading-8">
|
|
||||||
<p>
|
|
||||||
Nice to meet you! I am a Web Developer who specializes in architecting robust, user-centric
|
|
||||||
applications, transforming complex logic into seamless digital experiences.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
My work is driven by a commitment to meeting complex demands in specialized medical markets. With a
|
|
||||||
strong focus on ergonomics, UI/UX optimization, and precision within fast-paced clinical
|
|
||||||
environments, I engineer comprehensive EHR solutions—ranging from portable medical cart workstations
|
|
||||||
to pioneering tracking and fleet management systems.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
Beyond technical architecture, my work is deeply informed by an extensive background in organizational logistics. Grounded by foundational training in nationally recognized civic leadership programs, I previously directed one of the largest interest-driven communities in the Southern United States. In this capacity, I spearheaded large-scale event production—routinely managing gatherings of up to 250 attendees and coordinating cross-country travel for live entertainers. I have additionally facilitated strategic talent placements in commercial media, negotiated partnerships with major global streaming networks, and regularly presented as a panelist at large-scale conventions.
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p>
|
|
||||||
This focus-driven leadership approach directly translates to my engineering philosophy. Whether orchestrating high-concurrency state management systems, leading a project through rapid architectural shifts, or aligning a development team to meet ambitious goals, I am dedicated to executing with precision and creating software that is modern, accessible, and highly functional.
|
|
||||||
</p>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section className="mt-10 rounded-2xl bg-nord-2 p-8 font-cormorantGaramond text-white shadow-md">
|
|
||||||
<h2 className="mb-6 border-b border-white/20 pb-3 text-2xl font-semibold tracking-wide">
|
|
||||||
Focus Areas
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
<ul className="grid gap-4 text-lg md:grid-cols-2">
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-blue-300 shadow-sm"></span>
|
|
||||||
Clinical UI/UX & Ergonomics
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-blue-300 shadow-sm"></span>
|
|
||||||
EHR & Fleet Architecture
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-blue-300 shadow-sm"></span>
|
|
||||||
High-Concurrency Infrastructure
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-blue-300 shadow-sm"></span>
|
|
||||||
Large-Scale Event Logistics
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-blue-300 shadow-sm"></span>
|
|
||||||
Strategic Team Alignment
|
|
||||||
</li>
|
|
||||||
<li className="flex items-center gap-3">
|
|
||||||
<span className="h-2 w-2 rounded-full bg-blue-300 shadow-sm"></span>
|
|
||||||
Community State Management
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</section>
|
|
||||||
</main>
|
</main>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
title: "The art of the side hustle"
|
|
||||||
category: "business"
|
|
||||||
date: "02-02-2023"
|
|
||||||
---
|
|
||||||
|
|
||||||
# The art of the side hustle.
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
title: "Squirt"
|
|
||||||
category: "personal"
|
|
||||||
date: "29-06-2026"
|
|
||||||
---
|
|
||||||
|
|
||||||
# The Squirt Article
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Squirt is a playful and energetic topic that can represent movement, surprise, and a little bit of chaos. Sometimes, the
|
|
||||||
smallest things can make the biggest splash.
|
|
||||||
|
|
||||||
In everyday life, a squirt might be something simple: a splash of water, a squeeze from a bottle, or a sudden burst of
|
|
||||||
energy. It is quick, unexpected, and often memorable.
|
|
||||||
|
|
||||||
## A Small Burst of Fun
|
|
||||||
|
|
||||||
There is something fun about the word squirt. It feels lighthearted and informal. It does not take itself too seriously,
|
|
||||||
which makes it a great subject for a personal article.
|
|
||||||
|
|
||||||
Whether it reminds you of summer days, water fights, or tiny moments of surprise, squirt can be a symbol of simple joy.
|
|
||||||
|
|
||||||
## Final Thoughts
|
|
||||||
|
|
||||||
Not every article needs to be serious or complicated. Sometimes, writing about something small and silly is enough to
|
|
||||||
create a moment of fun.
|
|
||||||
|
|
||||||
Squirt may be simple, but it still makes a splash.
|
|
||||||
|
|
||||||
I had NO fucking idea what to put here, so, yeah, squirt.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
---
|
|
||||||
title: "Charles de Souza"
|
|
||||||
category: "personal"
|
|
||||||
date: "02-04-2023"
|
|
||||||
---
|
|
||||||
|
|
||||||
# Stress and management
|
|
||||||
23
components.json
Normal file
23
components.json
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://ui.shadcn.com/schema.json",
|
||||||
|
"style": "new-york",
|
||||||
|
"rsc": true,
|
||||||
|
"tsx": true,
|
||||||
|
"tailwind": {
|
||||||
|
"config": "",
|
||||||
|
"css": "app/globals.css",
|
||||||
|
"baseColor": "neutral",
|
||||||
|
"cssVariables": true,
|
||||||
|
"prefix": ""
|
||||||
|
},
|
||||||
|
"aliases": {
|
||||||
|
"components": "@/components",
|
||||||
|
"utils": "@/lib/utils",
|
||||||
|
"ui": "@/components/ui",
|
||||||
|
"lib": "@/lib",
|
||||||
|
"hooks": "@/hooks"
|
||||||
|
},
|
||||||
|
"registries": {
|
||||||
|
"@react-bits": "https://reactbits.dev/r/{name}.json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
import Link from "next/link"
|
|
||||||
import type {ArticleItem} from "@/types";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
category: string,
|
|
||||||
articles: ArticleItem[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const ArticleItemList = ({category, articles}: Props) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={"flex flex-col gap-5"}>
|
|
||||||
<h2 className={"font-cormorantGaramond text-4xl"}>{category}</h2>
|
|
||||||
<div className={"flex flex-col gap-2.5 font-poppins text-lg"}>
|
|
||||||
{articles.map((article, id) => (
|
|
||||||
<Link href={`/blog/${article.id}`} key={id}
|
|
||||||
className={"hover:text-nord-12 transition duration-250"}>
|
|
||||||
{article.title}
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ArticleItemList
|
|
||||||
766
components/GradientWaves.tsx
Normal file
766
components/GradientWaves.tsx
Normal file
|
|
@ -0,0 +1,766 @@
|
||||||
|
// From https://reactbits.dev/ - $ npx shadcn@latest add @react-bits/GradientWaves-TS-TW Aurora
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import { useEffect, useMemo, useRef, type CSSProperties } from 'react';
|
||||||
|
|
||||||
|
export interface GradientWavesProps {
|
||||||
|
horizonColor?: string;
|
||||||
|
waveColor?: string;
|
||||||
|
crestColor?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
lightMode?: boolean;
|
||||||
|
speed?: number;
|
||||||
|
amplitude?: number;
|
||||||
|
waveScale?: number;
|
||||||
|
waveRatio?: number;
|
||||||
|
swell?: number;
|
||||||
|
turbulence?: number;
|
||||||
|
tilt?: number;
|
||||||
|
zoom?: number;
|
||||||
|
height?: number;
|
||||||
|
fogDepth?: number;
|
||||||
|
relief?: number;
|
||||||
|
lightAngle?: number;
|
||||||
|
sheen?: number;
|
||||||
|
brightness?: number;
|
||||||
|
opacity?: number;
|
||||||
|
grain?: number;
|
||||||
|
quality?: 'low' | 'medium' | 'high';
|
||||||
|
intro?: boolean;
|
||||||
|
mouseInteraction?: boolean;
|
||||||
|
parallaxStrength?: number;
|
||||||
|
mouseStrength?: number;
|
||||||
|
swellOnClick?: boolean;
|
||||||
|
paused?: boolean;
|
||||||
|
dpr?: number;
|
||||||
|
className?: string;
|
||||||
|
style?: CSSProperties;
|
||||||
|
}
|
||||||
|
|
||||||
|
type RGB = [number, number, number];
|
||||||
|
|
||||||
|
interface Swell {
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
at: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface View {
|
||||||
|
zoom: number;
|
||||||
|
tilt: number;
|
||||||
|
height: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ROOT = 'relative h-full w-full overflow-hidden';
|
||||||
|
const CANVAS = 'absolute inset-0 block h-full w-full';
|
||||||
|
|
||||||
|
const MAX_SWELLS = 4;
|
||||||
|
const SWELL_SECONDS = 3.2;
|
||||||
|
const SWELL_GAIN = 2.8;
|
||||||
|
const SWELL_WIDTH = 8;
|
||||||
|
const INTRO_SECONDS = 2.6;
|
||||||
|
const CAMERA_HEIGHT = 30;
|
||||||
|
const QUALITY: Record<string, number> = { low: 56, medium: 84, high: 120 };
|
||||||
|
|
||||||
|
const VERTEX = `#version 300 es
|
||||||
|
in vec2 aPosition;
|
||||||
|
void main() {
|
||||||
|
gl_Position = vec4(aPosition, 0.0, 1.0);
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const FRAGMENT = `#version 300 es
|
||||||
|
precision highp float;
|
||||||
|
uniform vec2 uResolution;
|
||||||
|
uniform float uTime;
|
||||||
|
uniform float uClock;
|
||||||
|
uniform float uAmplitude;
|
||||||
|
uniform float uWaveScale;
|
||||||
|
uniform float uWaveRatio;
|
||||||
|
uniform float uSwell;
|
||||||
|
uniform float uTurbulence;
|
||||||
|
uniform float uHeight;
|
||||||
|
uniform float uTilt;
|
||||||
|
uniform float uZoom;
|
||||||
|
uniform vec2 uLook;
|
||||||
|
uniform float uFogDepth;
|
||||||
|
uniform float uBrightness;
|
||||||
|
uniform float uOpacity;
|
||||||
|
uniform float uGrain;
|
||||||
|
uniform vec3 uHorizon;
|
||||||
|
uniform vec3 uWave;
|
||||||
|
uniform vec3 uCrest;
|
||||||
|
uniform vec3 uBackground;
|
||||||
|
uniform float uOpaque;
|
||||||
|
uniform float uLight;
|
||||||
|
uniform vec3 uSun;
|
||||||
|
uniform float uRelief;
|
||||||
|
uniform float uSheen;
|
||||||
|
uniform float uSteps;
|
||||||
|
uniform float uStride;
|
||||||
|
uniform vec3 uIntro;
|
||||||
|
uniform vec3 uSwells[${MAX_SWELLS}];
|
||||||
|
uniform float uSwellWidth;
|
||||||
|
out vec4 outColor;
|
||||||
|
|
||||||
|
const float FAR = 4000.0;
|
||||||
|
|
||||||
|
float hash(vec2 p) {
|
||||||
|
vec3 p3 = fract(vec3(p.xyx) * 0.1031);
|
||||||
|
p3 += dot(p3, p3.yzx + 33.33);
|
||||||
|
return fract((p3.x + p3.y) * p3.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
vec3 surface(vec2 xy) {
|
||||||
|
vec2 freq = vec2(uWaveScale / 7.0, (uWaveScale * uWaveRatio) / 3.0);
|
||||||
|
vec4 tc = vec4(uTime / 0.130, uTime / 0.810, uTime / 0.200, uTime / 0.710);
|
||||||
|
float a = xy.x + tc.x;
|
||||||
|
float swellPhase = (xy.y + a) / 20.0 + tc.y;
|
||||||
|
float bend = uSwell * cos(swellPhase) / 20.0;
|
||||||
|
float mx = a + uSwell * sin(swellPhase);
|
||||||
|
float crossPhase = xy.x / 23.0 + tc.w;
|
||||||
|
float my = xy.y - tc.z + uTurbulence * cos(crossPhase);
|
||||||
|
float twist = -uTurbulence * sin(crossPhase) / 23.0;
|
||||||
|
float cx = cos(mx * freq.x);
|
||||||
|
float cy = cos(my * freq.y);
|
||||||
|
float rise = 1.0;
|
||||||
|
if (uIntro.z > 0.0) rise = mix(1.0, smoothstep(uIntro.y * 0.45, uIntro.y * 1.6, xy.x), uIntro.z);
|
||||||
|
float amp = uAmplitude * rise;
|
||||||
|
float h = (sin(mx * freq.x) + sin(my * freq.y)) * amp + uHeight;
|
||||||
|
float dx = amp * (freq.x * cx * (1.0 + bend) + freq.y * cy * twist);
|
||||||
|
float dy = amp * (freq.x * cx * bend + freq.y * cy);
|
||||||
|
for (int k = 0; k < ${MAX_SWELLS}; k++) {
|
||||||
|
vec3 sw = uSwells[k];
|
||||||
|
if (sw.z <= 0.0) continue;
|
||||||
|
float u = (xy.x - sw.x) / uSwellWidth;
|
||||||
|
float v = (xy.y - sw.y) / (uSwellWidth * 3.5);
|
||||||
|
float bump = sw.z * exp(-u * u - v * v);
|
||||||
|
h += bump;
|
||||||
|
dx += bump * (-2.0 * u / uSwellWidth);
|
||||||
|
dy += bump * (-2.0 * v / (uSwellWidth * 3.5));
|
||||||
|
}
|
||||||
|
return vec3(h, dx, dy);
|
||||||
|
}
|
||||||
|
|
||||||
|
float march(vec3 origin, vec3 dir) {
|
||||||
|
float dist = 0.0;
|
||||||
|
for (int i = 0; i < 128; i++) {
|
||||||
|
if (float(i) >= uSteps) break;
|
||||||
|
float gap = origin.z + dist * dir.z - surface(origin.xy + dist * dir.xy).x;
|
||||||
|
if (abs(gap) < 0.002 * dist + 0.01) break;
|
||||||
|
dist += uStride * gap;
|
||||||
|
if (!(abs(dist) < FAR)) return FAR;
|
||||||
|
}
|
||||||
|
return dist;
|
||||||
|
}
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
float c;
|
||||||
|
float s;
|
||||||
|
float vfov = (3.14159 / 2.3) / max(uZoom, 0.05);
|
||||||
|
vec3 cam = vec3(0.0, 0.0, ${CAMERA_HEIGHT.toFixed(1)});
|
||||||
|
vec2 uv = gl_FragCoord.xy / uResolution - 0.5;
|
||||||
|
uv.x *= uResolution.x / uResolution.y;
|
||||||
|
uv.y *= -1.0;
|
||||||
|
|
||||||
|
vec3 dir = vec3(0.0, 0.0, -1.0);
|
||||||
|
float ulen = length(uv);
|
||||||
|
float xrot = vfov * ulen;
|
||||||
|
c = cos(xrot);
|
||||||
|
s = sin(xrot);
|
||||||
|
dir = mat3(1.0, 0.0, 0.0, 0.0, c, -s, 0.0, s, c) * dir;
|
||||||
|
vec2 nuv = ulen > 1e-5 ? uv / ulen : vec2(1.0, 0.0);
|
||||||
|
dir = mat3(nuv.x, -nuv.y, 0.0, nuv.y, nuv.x, 0.0, 0.0, 0.0, 1.0) * dir;
|
||||||
|
c = cos(uTilt + uLook.y);
|
||||||
|
s = sin(uTilt + uLook.y);
|
||||||
|
dir = mat3(c, 0.0, s, 0.0, 1.0, 0.0, -s, 0.0, c) * dir;
|
||||||
|
c = cos(uLook.x);
|
||||||
|
s = sin(uLook.x);
|
||||||
|
dir = mat3(c, s, 0.0, -s, c, 0.0, 0.0, 0.0, 1.0) * dir;
|
||||||
|
|
||||||
|
float dist = march(cam, dir);
|
||||||
|
vec3 pos = cam + dist * dir;
|
||||||
|
vec3 field = surface(pos.xy);
|
||||||
|
vec3 view = -dir;
|
||||||
|
|
||||||
|
float crest = clamp((field.x - uHeight) / max(2.0 * uAmplitude, 0.001) * 0.5 + 0.5, 0.0, 1.0);
|
||||||
|
vec3 body = mix(uWave, uCrest, smoothstep(0.45, 1.0, crest) * 0.5);
|
||||||
|
vec3 normal = normalize(vec3(-field.yz * (1.0 + 1.5 * uRelief), 1.0));
|
||||||
|
float diffuse = clamp(dot(normal, uSun) * 0.5 + 0.5, 0.0, 1.0);
|
||||||
|
float lightness = mix(1.0, diffuse * diffuse * (3.0 - 2.0 * diffuse), uRelief);
|
||||||
|
float sheen = pow(clamp(dot(normal, normalize(uSun + view)), 0.0, 1.0), 14.0) * uSheen * (0.4 + 0.6 * uRelief);
|
||||||
|
float rim = pow(1.0 - clamp(dot(normal, view), 0.0, 1.0), 3.0);
|
||||||
|
|
||||||
|
vec3 col;
|
||||||
|
float alpha;
|
||||||
|
float haze = 1.0 - exp(-dist / (uFogDepth * 5.0));
|
||||||
|
float fade = 1.0 - smoothstep(0.35, 1.0, haze);
|
||||||
|
if (uLight < 0.5) {
|
||||||
|
vec3 lit = mix(uHorizon * 0.18, body, lightness) + uCrest * sheen;
|
||||||
|
lit = mix(lit, uHorizon, rim * 0.15 * uRelief);
|
||||||
|
col = mix(lit, uHorizon, smoothstep(0.0, 1.0, haze)) * 0.85 * uBrightness;
|
||||||
|
alpha = fade;
|
||||||
|
} else {
|
||||||
|
vec3 hue = mix(uHorizon, body, 0.35 + 0.65 * lightness);
|
||||||
|
float density = mix(0.16, 0.9, 1.0 - lightness);
|
||||||
|
density *= 1.0 - clamp(sheen, 0.0, 1.0) * 0.85;
|
||||||
|
density *= 1.0 - 0.5 * smoothstep(0.0, 1.0, haze);
|
||||||
|
col = hue;
|
||||||
|
alpha = fade * clamp(density * uBrightness, 0.0, 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
float reveal = uIntro.x;
|
||||||
|
col = clamp(col, 0.0, 1.0);
|
||||||
|
float grain = (hash(gl_FragCoord.xy + fract(uClock) * 64.0) - 0.5) * uGrain;
|
||||||
|
alpha = clamp(alpha * (1.0 + grain), 0.0, 1.0) * reveal * uOpacity;
|
||||||
|
if (uOpaque > 0.5) {
|
||||||
|
outColor = vec4(mix(uBackground, col, alpha), 1.0);
|
||||||
|
} else {
|
||||||
|
outColor = vec4(col * alpha, alpha);
|
||||||
|
}
|
||||||
|
}`;
|
||||||
|
|
||||||
|
const clamp = (value: number, min: number, max: number) =>
|
||||||
|
Math.min(max, Math.max(min, value));
|
||||||
|
|
||||||
|
const easeInOut = (t: number) =>
|
||||||
|
t < 0.5 ? 2 * t * t : 1 - Math.pow(-2 * t + 2, 2) / 2;
|
||||||
|
|
||||||
|
let probe: CanvasRenderingContext2D | null = null;
|
||||||
|
|
||||||
|
const parseColor = (value: string, fallback: RGB | null): RGB | null => {
|
||||||
|
if (value === 'transparent') return null;
|
||||||
|
if (typeof document === 'undefined') return fallback;
|
||||||
|
if (!probe) probe = document.createElement('canvas').getContext('2d');
|
||||||
|
if (!probe) return fallback;
|
||||||
|
probe.fillStyle = '#000000';
|
||||||
|
probe.fillStyle = value;
|
||||||
|
const text = probe.fillStyle;
|
||||||
|
if (text.startsWith('#')) {
|
||||||
|
const hex = parseInt(text.slice(1), 16);
|
||||||
|
return [
|
||||||
|
((hex >> 16) & 255) / 255,
|
||||||
|
((hex >> 8) & 255) / 255,
|
||||||
|
(hex & 255) / 255,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
const match =
|
||||||
|
/rgba?\(([\d.]+),\s*([\d.]+),\s*([\d.]+)(?:,\s*([\d.]+))?/.exec(text);
|
||||||
|
if (!match) return fallback;
|
||||||
|
if (match[4] !== undefined && Number(match[4]) === 0) return null;
|
||||||
|
return [
|
||||||
|
Number(match[1]) / 255,
|
||||||
|
Number(match[2]) / 255,
|
||||||
|
Number(match[3]) / 255,
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
const compile = (gl: WebGL2RenderingContext, type: number, source: string) => {
|
||||||
|
const shader = gl.createShader(type);
|
||||||
|
if (!shader) return null;
|
||||||
|
gl.shaderSource(shader, source);
|
||||||
|
gl.compileShader(shader);
|
||||||
|
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
||||||
|
console.error(gl.getShaderInfoLog(shader));
|
||||||
|
gl.deleteShader(shader);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return shader;
|
||||||
|
};
|
||||||
|
|
||||||
|
const rayAt = (point: number[], aspect: number, s: View, look: number[]) => {
|
||||||
|
const uvx = (point[0] - 0.5) * aspect;
|
||||||
|
const uvy = point[1] - 0.5;
|
||||||
|
const vfov = Math.PI / 2.3 / Math.max(s.zoom, 0.05);
|
||||||
|
const length = Math.hypot(uvx, uvy);
|
||||||
|
const turn = vfov * length;
|
||||||
|
let dir = [0, -Math.sin(turn), -Math.cos(turn)];
|
||||||
|
const nx = length > 1e-5 ? uvx / length : 1;
|
||||||
|
const ny = length > 1e-5 ? uvy / length : 0;
|
||||||
|
dir = [nx * dir[0] + ny * dir[1], -ny * dir[0] + nx * dir[1], dir[2]];
|
||||||
|
const pitch = s.tilt + look[1];
|
||||||
|
const cp = Math.cos(pitch);
|
||||||
|
const sp = Math.sin(pitch);
|
||||||
|
dir = [cp * dir[0] - sp * dir[2], dir[1], sp * dir[0] + cp * dir[2]];
|
||||||
|
const cy = Math.cos(look[0]);
|
||||||
|
const sy = Math.sin(look[0]);
|
||||||
|
dir = [cy * dir[0] - sy * dir[1], sy * dir[0] + cy * dir[1], dir[2]];
|
||||||
|
return dir;
|
||||||
|
};
|
||||||
|
|
||||||
|
const groundAt = (point: number[], aspect: number, s: View, look: number[]) => {
|
||||||
|
const dir = rayAt(point, aspect, s, look);
|
||||||
|
if (dir[2] > -0.02) return null;
|
||||||
|
const travel = (CAMERA_HEIGHT - s.height) / -dir[2];
|
||||||
|
return [dir[0] * travel, dir[1] * travel];
|
||||||
|
};
|
||||||
|
|
||||||
|
const prefersReduced = () =>
|
||||||
|
typeof window !== 'undefined' &&
|
||||||
|
(window.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false);
|
||||||
|
|
||||||
|
export default function GradientWaves({
|
||||||
|
horizonColor = '#5227FF',
|
||||||
|
waveColor = '#FF9FFC',
|
||||||
|
crestColor = '#FFFFFF',
|
||||||
|
backgroundColor = 'transparent',
|
||||||
|
lightMode = false,
|
||||||
|
speed = 0.4,
|
||||||
|
amplitude = 2.5,
|
||||||
|
waveScale = 0.6,
|
||||||
|
waveRatio = 0.9,
|
||||||
|
swell = 35,
|
||||||
|
turbulence = 20,
|
||||||
|
tilt = 64,
|
||||||
|
zoom = 1,
|
||||||
|
height = 5.5,
|
||||||
|
fogDepth = 15,
|
||||||
|
relief = 1,
|
||||||
|
lightAngle = 120,
|
||||||
|
sheen = 0,
|
||||||
|
brightness = 1,
|
||||||
|
opacity = 1,
|
||||||
|
grain = 0.2,
|
||||||
|
quality = 'medium',
|
||||||
|
intro = true,
|
||||||
|
mouseInteraction = true,
|
||||||
|
parallaxStrength = 0.5,
|
||||||
|
mouseStrength = 0.6,
|
||||||
|
swellOnClick = true,
|
||||||
|
paused = false,
|
||||||
|
dpr = 2,
|
||||||
|
className = '',
|
||||||
|
style,
|
||||||
|
}: GradientWavesProps) {
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const horizon = useMemo(
|
||||||
|
(): RGB => parseColor(horizonColor, [0.32, 0.15, 1]) ?? [0.32, 0.15, 1],
|
||||||
|
[horizonColor],
|
||||||
|
);
|
||||||
|
const wave = useMemo(
|
||||||
|
(): RGB => parseColor(waveColor, [1, 0.62, 0.99]) ?? [1, 0.62, 0.99],
|
||||||
|
[waveColor],
|
||||||
|
);
|
||||||
|
const crest = useMemo(
|
||||||
|
(): RGB => parseColor(crestColor, [1, 1, 1]) ?? [1, 1, 1],
|
||||||
|
[crestColor],
|
||||||
|
);
|
||||||
|
const paper = useMemo(
|
||||||
|
() => parseColor(backgroundColor, null),
|
||||||
|
[backgroundColor],
|
||||||
|
);
|
||||||
|
|
||||||
|
const settings = {
|
||||||
|
horizon,
|
||||||
|
wave,
|
||||||
|
crest,
|
||||||
|
paper,
|
||||||
|
lightMode,
|
||||||
|
speed,
|
||||||
|
amplitude: Math.max(0, amplitude),
|
||||||
|
waveScale: Math.max(0.01, waveScale),
|
||||||
|
waveRatio: Math.max(0.01, waveRatio),
|
||||||
|
swell,
|
||||||
|
turbulence,
|
||||||
|
tilt: (clamp(tilt, 10, 85) * Math.PI) / 180,
|
||||||
|
zoom: Math.max(0.05, zoom),
|
||||||
|
height,
|
||||||
|
fogDepth: Math.max(0.5, fogDepth),
|
||||||
|
relief: clamp(relief, 0, 1),
|
||||||
|
lightAngle: (lightAngle * Math.PI) / 180,
|
||||||
|
sheen: Math.max(0, sheen),
|
||||||
|
brightness: Math.max(0, brightness),
|
||||||
|
opacity: clamp(opacity, 0, 1),
|
||||||
|
grain:
|
||||||
|
typeof grain === 'number' ? Math.max(0, grain) : grain ? 0.05 : 0,
|
||||||
|
steps: QUALITY[quality] ?? QUALITY.medium,
|
||||||
|
intro,
|
||||||
|
mouseInteraction,
|
||||||
|
parallaxStrength: Math.max(0, parallaxStrength),
|
||||||
|
mouseStrength: clamp(mouseStrength, 0, 1),
|
||||||
|
swellOnClick,
|
||||||
|
paused,
|
||||||
|
dpr: Math.max(0.5, dpr),
|
||||||
|
};
|
||||||
|
const settingsRef = useRef(settings);
|
||||||
|
settingsRef.current = settings;
|
||||||
|
const wakeRef = useRef<(() => void) | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
wakeRef.current?.();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const container = containerRef.current;
|
||||||
|
if (!container) return undefined;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.className = CANVAS;
|
||||||
|
container.appendChild(canvas);
|
||||||
|
const gl = canvas.getContext('webgl2', {
|
||||||
|
alpha: true,
|
||||||
|
antialias: false,
|
||||||
|
depth: false,
|
||||||
|
stencil: false,
|
||||||
|
premultipliedAlpha: true,
|
||||||
|
powerPreference: 'high-performance',
|
||||||
|
});
|
||||||
|
if (!gl) {
|
||||||
|
canvas.remove();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const vertex = compile(gl, gl.VERTEX_SHADER, VERTEX);
|
||||||
|
const fragment = compile(gl, gl.FRAGMENT_SHADER, FRAGMENT);
|
||||||
|
const program = gl.createProgram();
|
||||||
|
if (!vertex || !fragment || !program) {
|
||||||
|
canvas.remove();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
gl.attachShader(program, vertex);
|
||||||
|
gl.attachShader(program, fragment);
|
||||||
|
gl.bindAttribLocation(program, 0, 'aPosition');
|
||||||
|
gl.linkProgram(program);
|
||||||
|
gl.deleteShader(vertex);
|
||||||
|
gl.deleteShader(fragment);
|
||||||
|
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
|
||||||
|
gl.deleteProgram(program);
|
||||||
|
canvas.remove();
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const names = [
|
||||||
|
'uResolution',
|
||||||
|
'uTime',
|
||||||
|
'uClock',
|
||||||
|
'uAmplitude',
|
||||||
|
'uWaveScale',
|
||||||
|
'uWaveRatio',
|
||||||
|
'uSwell',
|
||||||
|
'uTurbulence',
|
||||||
|
'uHeight',
|
||||||
|
'uTilt',
|
||||||
|
'uZoom',
|
||||||
|
'uLook',
|
||||||
|
'uFogDepth',
|
||||||
|
'uBrightness',
|
||||||
|
'uOpacity',
|
||||||
|
'uGrain',
|
||||||
|
'uHorizon',
|
||||||
|
'uWave',
|
||||||
|
'uCrest',
|
||||||
|
'uBackground',
|
||||||
|
'uOpaque',
|
||||||
|
'uLight',
|
||||||
|
'uSun',
|
||||||
|
'uRelief',
|
||||||
|
'uSheen',
|
||||||
|
'uSteps',
|
||||||
|
'uStride',
|
||||||
|
'uIntro',
|
||||||
|
'uSwells',
|
||||||
|
'uSwellWidth',
|
||||||
|
];
|
||||||
|
const u: Record<string, WebGLUniformLocation | null> = {};
|
||||||
|
names.forEach((name) => {
|
||||||
|
u[name] = gl.getUniformLocation(program, name);
|
||||||
|
});
|
||||||
|
const vao = gl.createVertexArray();
|
||||||
|
gl.bindVertexArray(vao);
|
||||||
|
const buffer = gl.createBuffer();
|
||||||
|
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
|
||||||
|
gl.bufferData(
|
||||||
|
gl.ARRAY_BUFFER,
|
||||||
|
new Float32Array([-1, -1, 3, -1, -1, 3]),
|
||||||
|
gl.STATIC_DRAW,
|
||||||
|
);
|
||||||
|
gl.enableVertexAttribArray(0);
|
||||||
|
gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);
|
||||||
|
gl.useProgram(program);
|
||||||
|
|
||||||
|
const swellData = new Float32Array(MAX_SWELLS * 3);
|
||||||
|
const state = {
|
||||||
|
width: 1,
|
||||||
|
height: 1,
|
||||||
|
cssWidth: 0,
|
||||||
|
cssHeight: 0,
|
||||||
|
time: 0,
|
||||||
|
clock: 0,
|
||||||
|
last: 0,
|
||||||
|
raf: 0,
|
||||||
|
visible: false,
|
||||||
|
introAt: -1,
|
||||||
|
pointer: [0.5, 0.5],
|
||||||
|
inside: false,
|
||||||
|
look: [0, 0],
|
||||||
|
steer: [0, 0],
|
||||||
|
swells: [] as Swell[],
|
||||||
|
quiet: 0,
|
||||||
|
seen: null as typeof settings | null,
|
||||||
|
};
|
||||||
|
|
||||||
|
const fit = () => {
|
||||||
|
const s = settingsRef.current;
|
||||||
|
const ratio = Math.min(window.devicePixelRatio || 1, s.dpr);
|
||||||
|
const width = Math.max(1, Math.round(state.cssWidth * ratio));
|
||||||
|
const height = Math.max(1, Math.round(state.cssHeight * ratio));
|
||||||
|
if (width === state.width && height === state.height) return;
|
||||||
|
state.quiet = 0;
|
||||||
|
state.width = width;
|
||||||
|
state.height = height;
|
||||||
|
canvas.width = width;
|
||||||
|
canvas.height = height;
|
||||||
|
};
|
||||||
|
|
||||||
|
const measure = () => {
|
||||||
|
state.cssWidth = container.clientWidth;
|
||||||
|
state.cssHeight = container.clientHeight;
|
||||||
|
fit();
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
const introProgress = (s: typeof settings, reduced: boolean) => {
|
||||||
|
if (!s.intro || reduced || state.introAt < 0) return 1;
|
||||||
|
return clamp((state.clock - state.introAt) / INTRO_SECONDS, 0, 1);
|
||||||
|
};
|
||||||
|
|
||||||
|
const render = (reduced: boolean) => {
|
||||||
|
const s = settingsRef.current;
|
||||||
|
gl.viewport(0, 0, state.width, state.height);
|
||||||
|
gl.uniform2f(u.uResolution, state.width, state.height);
|
||||||
|
gl.uniform1f(u.uTime, state.time);
|
||||||
|
gl.uniform1f(u.uClock, state.clock);
|
||||||
|
gl.uniform1f(u.uAmplitude, s.amplitude);
|
||||||
|
gl.uniform1f(u.uWaveScale, s.waveScale);
|
||||||
|
gl.uniform1f(u.uWaveRatio, s.waveRatio);
|
||||||
|
gl.uniform1f(u.uSwell, s.swell);
|
||||||
|
gl.uniform1f(u.uTurbulence, s.turbulence);
|
||||||
|
gl.uniform1f(u.uHeight, s.height);
|
||||||
|
gl.uniform1f(u.uTilt, s.tilt);
|
||||||
|
gl.uniform1f(u.uZoom, s.zoom);
|
||||||
|
gl.uniform2f(u.uLook, state.look[0], state.look[1]);
|
||||||
|
gl.uniform1f(u.uFogDepth, s.fogDepth);
|
||||||
|
gl.uniform1f(u.uBrightness, s.brightness);
|
||||||
|
gl.uniform1f(u.uOpacity, s.opacity);
|
||||||
|
gl.uniform1f(u.uGrain, s.grain);
|
||||||
|
gl.uniform3fv(u.uHorizon, s.horizon);
|
||||||
|
gl.uniform3fv(u.uWave, s.wave);
|
||||||
|
gl.uniform3fv(u.uCrest, s.crest);
|
||||||
|
const paperColor = s.paper ?? [0, 0, 0];
|
||||||
|
gl.uniform3f(
|
||||||
|
u.uBackground,
|
||||||
|
paperColor[0],
|
||||||
|
paperColor[1],
|
||||||
|
paperColor[2],
|
||||||
|
);
|
||||||
|
gl.uniform1f(u.uOpaque, s.paper ? 1 : 0);
|
||||||
|
gl.uniform1f(u.uLight, s.lightMode ? 1 : 0);
|
||||||
|
const elevation = 0.52;
|
||||||
|
const angle = s.lightAngle + state.steer[0] * s.mouseStrength * 2;
|
||||||
|
const altitude = elevation - state.steer[1] * s.mouseStrength * 0.7;
|
||||||
|
gl.uniform3f(
|
||||||
|
u.uSun,
|
||||||
|
Math.cos(altitude) * Math.cos(angle),
|
||||||
|
Math.cos(altitude) * Math.sin(angle),
|
||||||
|
Math.sin(altitude),
|
||||||
|
);
|
||||||
|
gl.uniform1f(u.uRelief, s.relief);
|
||||||
|
gl.uniform1f(u.uSheen, s.sheen);
|
||||||
|
gl.uniform1f(u.uSteps, s.steps);
|
||||||
|
const slope =
|
||||||
|
s.amplitude *
|
||||||
|
((s.waveScale / 7) * (1 + Math.abs(s.swell) / 20) +
|
||||||
|
((s.waveScale * s.waveRatio) / 3) *
|
||||||
|
(1 + Math.abs(s.turbulence) / 23));
|
||||||
|
const progress = introProgress(s, reduced);
|
||||||
|
const shown = Math.min(1, progress / 0.22);
|
||||||
|
const crest =
|
||||||
|
160 *
|
||||||
|
Math.pow(
|
||||||
|
3 / 160,
|
||||||
|
easeInOut(Math.max(0, (progress - 0.1) / 0.9)),
|
||||||
|
);
|
||||||
|
const settle = clamp((progress - 0.75) / 0.25, 0, 1);
|
||||||
|
gl.uniform3f(
|
||||||
|
u.uIntro,
|
||||||
|
shown * shown * (3 - 2 * shown),
|
||||||
|
crest,
|
||||||
|
1 - settle * settle * (3 - 2 * settle),
|
||||||
|
);
|
||||||
|
swellData.fill(0);
|
||||||
|
let swellPeak = 0;
|
||||||
|
state.swells.forEach((swell, index) => {
|
||||||
|
const age = state.clock - swell.at;
|
||||||
|
const life = age / SWELL_SECONDS;
|
||||||
|
const envelope =
|
||||||
|
(1 - Math.exp(-age / 0.25)) * Math.pow(1 - life, 1.6);
|
||||||
|
const lift = s.amplitude * SWELL_GAIN * envelope;
|
||||||
|
swellData.set([swell.x - age * 12, swell.y, lift], index * 3);
|
||||||
|
swellPeak = Math.max(swellPeak, lift);
|
||||||
|
});
|
||||||
|
gl.uniform3fv(u.uSwells, swellData);
|
||||||
|
gl.uniform1f(u.uSwellWidth, SWELL_WIDTH);
|
||||||
|
gl.uniform1f(
|
||||||
|
u.uStride,
|
||||||
|
0.9 /
|
||||||
|
Math.max(
|
||||||
|
1,
|
||||||
|
(slope + (swellPeak * 0.86) / SWELL_WIDTH) / 1.5,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
gl.clearColor(0, 0, 0, 0);
|
||||||
|
gl.clear(gl.COLOR_BUFFER_BIT);
|
||||||
|
gl.drawArrays(gl.TRIANGLES, 0, 3);
|
||||||
|
};
|
||||||
|
|
||||||
|
const tick = (now: number) => {
|
||||||
|
state.raf = 0;
|
||||||
|
if (!state.visible || document.hidden) return;
|
||||||
|
const s = settingsRef.current;
|
||||||
|
const dt = state.last
|
||||||
|
? Math.min((now - state.last) / 1000, 0.1)
|
||||||
|
: 0;
|
||||||
|
state.last = now;
|
||||||
|
const reduced = prefersReduced();
|
||||||
|
state.clock += dt;
|
||||||
|
if (state.introAt < 0) state.introAt = state.clock;
|
||||||
|
if (!s.paused && !reduced) state.time += dt * s.speed;
|
||||||
|
|
||||||
|
const steering = s.mouseInteraction && state.inside && !reduced;
|
||||||
|
const target = steering ? state.pointer : [0.5, 0.5];
|
||||||
|
const ease = 1 - Math.exp(-dt / 0.45);
|
||||||
|
const goalX = (target[0] - 0.5) * s.parallaxStrength * 0.35;
|
||||||
|
const goalY = (target[1] - 0.5) * s.parallaxStrength * 0.18;
|
||||||
|
state.look[0] += (goalX - state.look[0]) * ease;
|
||||||
|
state.look[1] += (goalY - state.look[1]) * ease;
|
||||||
|
const moving =
|
||||||
|
Math.abs(goalX - state.look[0]) +
|
||||||
|
Math.abs(goalY - state.look[1]) >
|
||||||
|
0.0002;
|
||||||
|
|
||||||
|
const steerEase = 1 - Math.exp(-dt / 0.6);
|
||||||
|
const steerX = steering ? state.pointer[0] - 0.5 : 0;
|
||||||
|
const steerY = steering ? 0.5 - state.pointer[1] : 0;
|
||||||
|
state.steer[0] += (steerX - state.steer[0]) * steerEase;
|
||||||
|
state.steer[1] += (steerY - state.steer[1]) * steerEase;
|
||||||
|
const turning =
|
||||||
|
Math.abs(steerX - state.steer[0]) +
|
||||||
|
Math.abs(steerY - state.steer[1]) >
|
||||||
|
0.0005;
|
||||||
|
state.swells = state.swells.filter(
|
||||||
|
(swell) => state.clock - swell.at < SWELL_SECONDS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const still = s.paused || reduced;
|
||||||
|
const transient =
|
||||||
|
moving ||
|
||||||
|
turning ||
|
||||||
|
state.swells.length > 0 ||
|
||||||
|
introProgress(s, reduced) < 1;
|
||||||
|
if (state.seen !== s || transient || !still) state.quiet = 0;
|
||||||
|
else state.quiet += 1;
|
||||||
|
state.seen = s;
|
||||||
|
if (state.quiet > 3) return;
|
||||||
|
|
||||||
|
state.raf = requestAnimationFrame(tick);
|
||||||
|
fit();
|
||||||
|
render(reduced);
|
||||||
|
};
|
||||||
|
|
||||||
|
const wake = () => {
|
||||||
|
if (state.raf || !state.visible || document.hidden) return;
|
||||||
|
state.last = 0;
|
||||||
|
state.raf = requestAnimationFrame(tick);
|
||||||
|
};
|
||||||
|
wakeRef.current = wake;
|
||||||
|
|
||||||
|
const local = (event: PointerEvent) => {
|
||||||
|
const rect = container.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
x: (event.clientX - rect.left) / Math.max(rect.width, 1),
|
||||||
|
y: (event.clientY - rect.top) / Math.max(rect.height, 1),
|
||||||
|
inside:
|
||||||
|
event.clientX >= rect.left &&
|
||||||
|
event.clientX <= rect.right &&
|
||||||
|
event.clientY >= rect.top &&
|
||||||
|
event.clientY <= rect.bottom,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const onMove = (event: PointerEvent) => {
|
||||||
|
const point = local(event);
|
||||||
|
const was = state.inside;
|
||||||
|
state.inside = point.inside;
|
||||||
|
if (point.inside) state.pointer = [point.x, point.y];
|
||||||
|
if ((point.inside || was) && settingsRef.current.mouseInteraction)
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onLeave = () => {
|
||||||
|
state.inside = false;
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDown = (event: PointerEvent) => {
|
||||||
|
const s = settingsRef.current;
|
||||||
|
if (!s.swellOnClick || event.button !== 0 || prefersReduced())
|
||||||
|
return;
|
||||||
|
if (
|
||||||
|
event.target instanceof Element &&
|
||||||
|
event.target.closest(
|
||||||
|
'a, button, input, textarea, select, label, [role="button"]',
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return;
|
||||||
|
const point = local(event);
|
||||||
|
if (!point.inside) return;
|
||||||
|
const ground = groundAt(
|
||||||
|
[point.x, point.y],
|
||||||
|
state.width / Math.max(state.height, 1),
|
||||||
|
s,
|
||||||
|
state.look,
|
||||||
|
);
|
||||||
|
if (!ground) return;
|
||||||
|
state.swells.push({ x: ground[0], y: ground[1], at: state.clock });
|
||||||
|
if (state.swells.length > MAX_SWELLS) state.swells.shift();
|
||||||
|
wake();
|
||||||
|
};
|
||||||
|
|
||||||
|
const visibility = new IntersectionObserver((entries) => {
|
||||||
|
state.visible = entries.some((entry) => entry.isIntersecting);
|
||||||
|
wake();
|
||||||
|
});
|
||||||
|
visibility.observe(container);
|
||||||
|
const sizes = new ResizeObserver(measure);
|
||||||
|
sizes.observe(container);
|
||||||
|
measure();
|
||||||
|
document.addEventListener('visibilitychange', wake);
|
||||||
|
window.addEventListener('pointermove', onMove, { passive: true });
|
||||||
|
window.addEventListener('pointerdown', onDown);
|
||||||
|
document.documentElement.addEventListener('pointerleave', onLeave);
|
||||||
|
state.raf = requestAnimationFrame(tick);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
cancelAnimationFrame(state.raf);
|
||||||
|
wakeRef.current = null;
|
||||||
|
document.removeEventListener('visibilitychange', wake);
|
||||||
|
visibility.disconnect();
|
||||||
|
sizes.disconnect();
|
||||||
|
window.removeEventListener('pointermove', onMove);
|
||||||
|
window.removeEventListener('pointerdown', onDown);
|
||||||
|
document.documentElement.removeEventListener(
|
||||||
|
'pointerleave',
|
||||||
|
onLeave,
|
||||||
|
);
|
||||||
|
gl.deleteProgram(program);
|
||||||
|
gl.deleteBuffer(buffer);
|
||||||
|
gl.deleteVertexArray(vao);
|
||||||
|
gl.getExtension('WEBGL_lose_context')?.loseContext();
|
||||||
|
canvas.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={[ROOT, className].filter(Boolean).join(' ')}
|
||||||
|
style={style}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
21
components/layout/layout-footer.tsx
Normal file
21
components/layout/layout-footer.tsx
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
import { site } from '@/lib/site';
|
||||||
|
|
||||||
|
const Footer = () => (
|
||||||
|
<footer className="border-t border-white/10 bg-obsidian/40 backdrop-blur-sm">
|
||||||
|
<div className="container-page flex items-center justify-between py-6 text-xs text-ink/65">
|
||||||
|
<p>
|
||||||
|
© {new Date().getFullYear()} {site.name}
|
||||||
|
</p>
|
||||||
|
<a
|
||||||
|
href={site.gitlab}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="transition-colors hover:text-aurora"
|
||||||
|
>
|
||||||
|
GitLab ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Footer;
|
||||||
|
|
@ -1,68 +1,103 @@
|
||||||
'use client'
|
'use client';
|
||||||
|
|
||||||
import React, {useState} from 'react'
|
import { useState } from 'react';
|
||||||
import Image from 'next/image'
|
import Image from 'next/image';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import Logo from "../../public/borb.png"
|
import { Bars3Icon, XMarkIcon } from '@heroicons/react/24/outline';
|
||||||
|
import Logo from '@/public/borb.png';
|
||||||
|
import { nav, site } from '@/lib/site';
|
||||||
|
|
||||||
const Header = () => {
|
const Header = () => {
|
||||||
const [navbar, setNavbar] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const close = () => setOpen(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<header className="sticky top-0 z-50 border-b border-white/10 bg-obsidian/45 backdrop-blur-md">
|
||||||
<header className="font-poppins bg-polar-night-light z-10">
|
<div className="container-page flex h-16 items-center justify-between">
|
||||||
<div className="justify-between px-2 lg:px-0 mx-auto lg:max-w-7xl md:items-center md:flex md:px-4">
|
<Link
|
||||||
<div>
|
href="/"
|
||||||
<div className="flex items-center justify-between md:block">
|
onClick={close}
|
||||||
<div className='flex items-center justify-between md:py-3 gap-10'>
|
className="flex items-center gap-2.5 text-sm font-medium tracking-tight text-ink"
|
||||||
<Link href={"/"} className="flex flex-col items-center gap-0 leading-none">
|
aria-label="Home"
|
||||||
<Image src={Logo} width={100} alt='Logo'/>
|
>
|
||||||
|
<Image
|
||||||
|
src={Logo}
|
||||||
|
alt=""
|
||||||
|
width={40}
|
||||||
|
height={40}
|
||||||
|
loading="eager"
|
||||||
|
className="h-9 w-9 object-contain"
|
||||||
|
/>
|
||||||
|
<span>{site.name}</span>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
|
||||||
<div className="md:hidden">
|
<nav
|
||||||
|
aria-label="Primary"
|
||||||
|
className="hidden items-center gap-7 text-sm md:flex"
|
||||||
|
>
|
||||||
|
{nav.map((item) => (
|
||||||
|
<a
|
||||||
|
key={item.href}
|
||||||
|
href={item.href}
|
||||||
|
className="text-ink/75 transition-colors hover:text-ink"
|
||||||
|
>
|
||||||
|
{item.label}
|
||||||
|
</a>
|
||||||
|
))}
|
||||||
|
<a
|
||||||
|
href={site.gitlab}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="border-l border-white/15 pl-6 text-aurora transition-colors hover:text-crest"
|
||||||
|
>
|
||||||
|
GitLab ↗
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
className="p-3 text-gray-700 rounded-md outline-none focus:border-gray-400 border-2 focus:border bg-nord-10"
|
type="button"
|
||||||
onClick={() => setNavbar(!navbar)}>
|
className="grid h-9 w-9 place-items-center border border-white/15 text-ink hover:border-aurora md:hidden"
|
||||||
{navbar ? (
|
aria-expanded={open}
|
||||||
<div className="bar text-snow-storm"></div>
|
aria-controls="mobile-nav"
|
||||||
|
aria-label={open ? 'Close menu' : 'Open menu'}
|
||||||
|
onClick={() => setOpen((v) => !v)}
|
||||||
|
>
|
||||||
|
{open ? (
|
||||||
|
<XMarkIcon className="h-4 w-4" aria-hidden />
|
||||||
) : (
|
) : (
|
||||||
<div className="close text-snow-storm"></div>
|
<Bars3Icon className="h-4 w-4" aria-hidden />
|
||||||
)}
|
)}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<hr className='md:opacity-0 md:pt-3'></hr>
|
|
||||||
<div>
|
|
||||||
<div
|
|
||||||
className={`flex-1 justify-self-center md:block md:pb-0 md:mt-0 ${navbar ? 'p-12 md:p-0 block' : 'hidden'}`}>
|
|
||||||
<ul className=" md:h-auto items-center justify-center md:flex">
|
|
||||||
<li className="text-xl text-snow-storm py-2 my-2 md:my-0 md:px-6 text-center md:border-b-0 border-snow-storm-dark hover:text-nord-12 transition duration-250">
|
|
||||||
<Link href="/" onClick={() => setNavbar(!navbar)}
|
|
||||||
className='sm:px-52 md:px-0'>Home</Link>
|
|
||||||
</li>
|
|
||||||
|
|
||||||
<li className="text-xl text-snow-storm py-2 my-2 md:my-0 px-6 text-center md:border-b-0 border-snow-storm-dark hover:text-nord-12 transition duration-250">
|
{open && (
|
||||||
<Link href="/blog" onClick={() => setNavbar(!navbar)}
|
<nav
|
||||||
className='sm:px-52 md:px-0'>Blog</Link>
|
id="mobile-nav"
|
||||||
</li>
|
aria-label="Mobile"
|
||||||
|
className="flex flex-col border-t border-white/10 bg-obsidian/90 backdrop-blur-md md:hidden"
|
||||||
|
>
|
||||||
<li className="text-xl text-snow-storm py-2 my-2 md:my-0 px-6 text-center md:border-b-0 border-snow-storm-dark hover:text-nord-12 transition duration-250">
|
{nav.map((item) => (
|
||||||
<Link href="https://gitlab.com/caydens" onClick={() => setNavbar(!navbar)}
|
<a
|
||||||
className='sm:px-48 md:px-0'>GitLab</Link>
|
key={item.href}
|
||||||
</li>
|
href={item.href}
|
||||||
|
onClick={close}
|
||||||
<li className="text-xl text-snow-storm py-2 my-2 md:my-0 px-6 text-center md:border-b-0 border-snow-storm-dark hover:text-nord-12 transition duration-250">
|
className="border-b border-white/10 px-6 py-3.5 text-sm text-ink/80 hover:bg-white/5 hover:text-ink"
|
||||||
<Link href="/contact" onClick={() => setNavbar(!navbar)}
|
>
|
||||||
className='sm:px-48 md:px-0'>Contact</Link>
|
{item.label}
|
||||||
</li>
|
</a>
|
||||||
</ul>
|
))}
|
||||||
</div>
|
<a
|
||||||
</div>
|
href={site.gitlab}
|
||||||
</div>
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="px-6 py-3.5 text-sm text-aurora hover:bg-white/5"
|
||||||
|
>
|
||||||
|
GitLab ↗
|
||||||
|
</a>
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
</header>
|
</header>
|
||||||
</>
|
);
|
||||||
)
|
};
|
||||||
}
|
|
||||||
export default Header
|
export default Header;
|
||||||
|
|
|
||||||
28
components/layout/page-background.tsx
Normal file
28
components/layout/page-background.tsx
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
import GradientWaves from '@/components/GradientWaves';
|
||||||
|
|
||||||
|
// React Bits GradientWaves "Aurora" preset, tuned to fill the viewport.
|
||||||
|
const PageBackground = () => (
|
||||||
|
<div aria-hidden className="fixed inset-0 -z-10 overflow-hidden">
|
||||||
|
<GradientWaves
|
||||||
|
horizonColor="#064e3b"
|
||||||
|
waveColor="#34d399"
|
||||||
|
crestColor="#d1fae5"
|
||||||
|
speed={0.35}
|
||||||
|
amplitude={2.6}
|
||||||
|
waveScale={0.7}
|
||||||
|
waveRatio={0.9}
|
||||||
|
swell={38}
|
||||||
|
turbulence={24}
|
||||||
|
fogDepth={12}
|
||||||
|
lightAngle={170}
|
||||||
|
sheen={0.28}
|
||||||
|
height={5.5}
|
||||||
|
tilt={64}
|
||||||
|
className="h-full w-full"
|
||||||
|
/>
|
||||||
|
{/* Dark wash keeps foreground text readable */}
|
||||||
|
<div className="absolute inset-0 bg-obsidian/25" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default PageBackground;
|
||||||
32
components/sections/contact.tsx
Normal file
32
components/sections/contact.tsx
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { site } from '@/lib/site';
|
||||||
|
|
||||||
|
const Contact = () => (
|
||||||
|
<section id="contact" className="container-page pt-12 pb-24">
|
||||||
|
<div className="flex flex-col items-start justify-between gap-6 panel p-6 sm:flex-row sm:items-center md:p-9">
|
||||||
|
<div>
|
||||||
|
<h2 className="font-display text-2xl font-medium tracking-tight text-ink md:text-3xl">
|
||||||
|
Get in touch
|
||||||
|
</h2>
|
||||||
|
<p className="mt-1.5 text-sm text-ink/70">
|
||||||
|
Have a project, role, or panel in mind? Let's talk.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-wrap gap-3 text-sm font-medium">
|
||||||
|
<a href={`mailto:${site.email}`} className="btn-primary">
|
||||||
|
{site.email} →
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={site.gitlab}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="btn-ghost"
|
||||||
|
>
|
||||||
|
GitLab ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Contact;
|
||||||
31
components/sections/feats.tsx
Normal file
31
components/sections/feats.tsx
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
import Reveal from '@/components/ui/reveal';
|
||||||
|
import SectionHeading from '@/components/ui/section-heading';
|
||||||
|
import { feats } from '@/lib/site';
|
||||||
|
|
||||||
|
const Feats = () => (
|
||||||
|
<section id="feats" className="container-page py-16">
|
||||||
|
<SectionHeading title="Leadership">
|
||||||
|
Operations and community work outside of code.
|
||||||
|
</SectionHeading>
|
||||||
|
|
||||||
|
<ul className="grid panel sm:grid-cols-2">
|
||||||
|
{feats.map((feat, i) => (
|
||||||
|
<li
|
||||||
|
key={feat.title}
|
||||||
|
className="border-white/12 p-6 transition-colors not-last:border-b hover:bg-white/[0.04] sm:odd:border-r sm:odd:nth-last-2:border-b-0 md:p-7"
|
||||||
|
>
|
||||||
|
<Reveal delay={i * 60}>
|
||||||
|
<h3 className="font-display text-base font-medium text-ink">
|
||||||
|
{feat.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-1.5 text-sm leading-relaxed text-ink/70">
|
||||||
|
{feat.detail}
|
||||||
|
</p>
|
||||||
|
</Reveal>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Feats;
|
||||||
69
components/sections/hero.tsx
Normal file
69
components/sections/hero.tsx
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { about, site, stack } from '@/lib/site';
|
||||||
|
|
||||||
|
const Hero = () => (
|
||||||
|
<section id="about" className="container-page py-20 md:pt-28 md:pb-24">
|
||||||
|
<div className="max-w-2xl">
|
||||||
|
<p className="mb-4 flex items-center gap-3 font-mono text-xs tracking-[0.2em] text-aurora uppercase">
|
||||||
|
<span aria-hidden className="h-px w-6 bg-aurora" />
|
||||||
|
<span>{site.role}</span>
|
||||||
|
</p>
|
||||||
|
<h1 className="font-display text-5xl font-semibold tracking-[-0.035em] text-ink sm:text-6xl md:text-7xl">
|
||||||
|
Howdy, I'm {site.name}.
|
||||||
|
</h1>
|
||||||
|
<p className="mt-4 max-w-xl text-lg leading-relaxed text-ink/85 md:text-xl">
|
||||||
|
{site.tagline}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className="mt-8 flex flex-wrap gap-3 text-sm font-medium">
|
||||||
|
<a href="#work" className="btn-primary">
|
||||||
|
Selected work ↓
|
||||||
|
</a>
|
||||||
|
<a
|
||||||
|
href={site.gitlab}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
className="btn-ghost"
|
||||||
|
>
|
||||||
|
GitLab ↗
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-16 grid panel md:mt-20 md:grid-cols-[14rem_1fr]">
|
||||||
|
<div className="border-b border-white/12 p-5 md:border-r md:border-b-0 md:p-6">
|
||||||
|
<div className="relative aspect-[8/7] overflow-hidden border border-white/12 bg-obsidian/60">
|
||||||
|
<Image
|
||||||
|
src="/cayden-portrait.png"
|
||||||
|
alt="Cayden"
|
||||||
|
fill
|
||||||
|
loading="eager"
|
||||||
|
sizes="(min-width: 768px) 13rem, 90vw"
|
||||||
|
className="object-cover"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col justify-between">
|
||||||
|
<div className="space-y-4 p-6 text-[15.5px] leading-relaxed text-ink/85 md:p-8">
|
||||||
|
{about.map((paragraph) => (
|
||||||
|
<p key={paragraph}>{paragraph}</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul className="flex flex-wrap gap-x-6 gap-y-2 border-t border-white/12 px-6 py-3.5 font-mono text-xs text-crest/80 md:px-8">
|
||||||
|
{stack.map((tech) => (
|
||||||
|
<li
|
||||||
|
key={tech}
|
||||||
|
className="transition-colors hover:text-aurora"
|
||||||
|
>
|
||||||
|
{tech}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Hero;
|
||||||
34
components/sections/projects.tsx
Normal file
34
components/sections/projects.tsx
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
import Reveal from '@/components/ui/reveal';
|
||||||
|
import SectionHeading from '@/components/ui/section-heading';
|
||||||
|
import { projects } from '@/lib/site';
|
||||||
|
|
||||||
|
const Projects = () => (
|
||||||
|
<section id="work" className="container-page py-16">
|
||||||
|
<SectionHeading title="Selected Work">
|
||||||
|
Instant-Response Web Applications
|
||||||
|
</SectionHeading>
|
||||||
|
|
||||||
|
<div className="grid divide-y divide-white/12 panel md:grid-cols-3 md:divide-x md:divide-y-0">
|
||||||
|
{projects.map((project, i) => (
|
||||||
|
<Reveal key={project.title} delay={i * 60}>
|
||||||
|
<article className="flex h-full flex-col justify-between p-6 transition-colors hover:bg-white/4 md:p-7">
|
||||||
|
<div>
|
||||||
|
<h3 className="font-display text-lg font-medium tracking-tight text-ink">
|
||||||
|
{project.title}
|
||||||
|
</h3>
|
||||||
|
<p className="mt-2.5 text-sm leading-relaxed text-ink/70">
|
||||||
|
{project.blurb}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="mt-8 border-t border-white/10 pt-3 font-mono text-xs text-aurora/90">
|
||||||
|
{project.stack}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
</Reveal>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Projects;
|
||||||
49
components/ui/reveal.tsx
Normal file
49
components/ui/reveal.tsx
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
'use client';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useEffect,
|
||||||
|
useRef,
|
||||||
|
useState,
|
||||||
|
type CSSProperties,
|
||||||
|
type ReactNode,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
children: ReactNode;
|
||||||
|
delay?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Fades content up as it scrolls into view.
|
||||||
|
const Reveal = ({ children, delay = 0 }: Props) => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
const [visible, setVisible] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const node = ref.current;
|
||||||
|
if (!node) return;
|
||||||
|
const observer = new IntersectionObserver(
|
||||||
|
([entry]) => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
setVisible(true);
|
||||||
|
observer.disconnect();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ threshold: 0.12 },
|
||||||
|
);
|
||||||
|
observer.observe(node);
|
||||||
|
return () => observer.disconnect();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
data-visible={visible}
|
||||||
|
className="reveal"
|
||||||
|
style={{ '--reveal-delay': `${delay}ms` } as CSSProperties}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Reveal;
|
||||||
20
components/ui/section-heading.tsx
Normal file
20
components/ui/section-heading.tsx
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
title: string;
|
||||||
|
children?: ReactNode;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SectionHeading = ({ title, children }: Props) => (
|
||||||
|
<div className="mb-6 flex flex-col justify-between gap-2 border-b border-white/15 pb-4 sm:flex-row sm:items-end">
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<span aria-hidden className="h-px w-5 bg-aurora" />
|
||||||
|
<h2 className="font-display text-2xl font-medium tracking-tight text-ink md:text-3xl">
|
||||||
|
{title}
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
{children && <p className="text-sm text-ink/70">{children}</p>}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SectionHeading;
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { defineConfig, globalIgnores } from "eslint/config";
|
import { defineConfig, globalIgnores } from 'eslint/config';
|
||||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
import nextVitals from 'eslint-config-next/core-web-vitals';
|
||||||
import nextTs from "eslint-config-next/typescript";
|
import nextTs from 'eslint-config-next/typescript';
|
||||||
|
|
||||||
const eslintConfig = defineConfig([
|
const eslintConfig = defineConfig([
|
||||||
...nextVitals,
|
...nextVitals,
|
||||||
|
|
@ -8,10 +8,10 @@ const eslintConfig = defineConfig([
|
||||||
// Override default ignores of eslint-config-next.
|
// Override default ignores of eslint-config-next.
|
||||||
globalIgnores([
|
globalIgnores([
|
||||||
// Default ignores of eslint-config-next:
|
// Default ignores of eslint-config-next:
|
||||||
".next/**",
|
'.next/**',
|
||||||
"out/**",
|
'out/**',
|
||||||
"build/**",
|
'build/**',
|
||||||
"next-env.d.ts",
|
'next-env.d.ts',
|
||||||
]),
|
]),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
import fs from "fs"
|
|
||||||
import matter from "gray-matter"
|
|
||||||
import path from "path"
|
|
||||||
import moment from "moment"
|
|
||||||
import {remark} from "remark";
|
|
||||||
import html from "remark-html"
|
|
||||||
|
|
||||||
import type {ArticleItem} from "@/types";
|
|
||||||
|
|
||||||
const articlesDirectory = path.join(process.cwd(), "articles");
|
|
||||||
|
|
||||||
export const getSortedArticles = (): ArticleItem[] => {
|
|
||||||
const fileNames = fs.readdirSync(articlesDirectory);
|
|
||||||
|
|
||||||
const allArticlesData = fileNames.map((fileName) => {
|
|
||||||
const id = fileName.replace(/\.md$/, "");
|
|
||||||
|
|
||||||
const fullPath = path.join(articlesDirectory, fileName);
|
|
||||||
const fileContents = fs.readFileSync(fullPath, "utf8");
|
|
||||||
|
|
||||||
const matterResult = matter(fileContents);
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
title: matterResult.data.title,
|
|
||||||
date: matterResult.data.date,
|
|
||||||
category: matterResult.data.category,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
return allArticlesData.sort((a, b) => {
|
|
||||||
const format = "DD-MM-YYYY";
|
|
||||||
const dateOne = moment(a.date, format);
|
|
||||||
const dateTwo = moment(b.date, format);
|
|
||||||
|
|
||||||
return dateTwo.valueOf() - dateOne.valueOf();
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getCategorizedArticles = (): Record<string, ArticleItem[]> => {
|
|
||||||
const sortedArticles = getSortedArticles();
|
|
||||||
const categorisedArticles: Record<string, ArticleItem[]> = {}
|
|
||||||
|
|
||||||
sortedArticles.forEach((article) => {
|
|
||||||
if (!categorisedArticles[article.category]) {
|
|
||||||
categorisedArticles[article.category] = []
|
|
||||||
}
|
|
||||||
categorisedArticles[article.category].push(article);
|
|
||||||
})
|
|
||||||
|
|
||||||
return categorisedArticles;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getArticleData = async (id: string) => {
|
|
||||||
const fullPath = path.join(articlesDirectory, `${id}.md`);
|
|
||||||
|
|
||||||
const fileContents = fs.readFileSync(fullPath, "utf8");
|
|
||||||
|
|
||||||
const matterResult = matter(fileContents);
|
|
||||||
|
|
||||||
const processedContent = await remark()
|
|
||||||
.use(html)
|
|
||||||
.process(matterResult.content)
|
|
||||||
|
|
||||||
const contentHtml = processedContent.toString();
|
|
||||||
|
|
||||||
return {
|
|
||||||
id,
|
|
||||||
contentHtml,
|
|
||||||
title: matterResult.data.title,
|
|
||||||
category: matterResult.data.category,
|
|
||||||
date: moment(matterResult.data.date, "DD-MM-YYYY").format("MMMM Do YYYY")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
70
lib/site.ts
Normal file
70
lib/site.ts
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
export type Project = {
|
||||||
|
title: string;
|
||||||
|
blurb: string;
|
||||||
|
stack: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Feat = {
|
||||||
|
title: string;
|
||||||
|
detail: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const site = {
|
||||||
|
name: 'Cayden',
|
||||||
|
role: 'Web Developer',
|
||||||
|
tagline:
|
||||||
|
"I'm a proven people leader building fast, dependable web software for clinical floors and fleet operations.",
|
||||||
|
gitlab: 'https://gitlab.com/caydens',
|
||||||
|
email: 'hi@caydens.dev',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nav = [
|
||||||
|
{ label: 'About', href: '#about' },
|
||||||
|
{ label: 'Work', href: '#work' },
|
||||||
|
{ label: 'Leadership', href: '#feats' },
|
||||||
|
{ label: 'Contact', href: '#contact' },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const stack = ['TypeScript', 'React', 'Next.js', 'Python', 'JavaScript'];
|
||||||
|
|
||||||
|
export const about = [
|
||||||
|
'I engineer responsive, high-performance web applications where speed and clarity are paramount, specializing in touch-first EHR interfaces and live fleet telemetry for mobile medical workstations. I bridge the gap between human workflows and technical architecture to deliver interfaces that feel natural, comfortable, and precise.',
|
||||||
|
"Outside of software engineering, I directed one of the Southeast’s largest interest-driven communities, producing 250-attendee live events, speaking at regional conventions, and coordinating partnerships with major streaming platforms.",
|
||||||
|
];
|
||||||
|
|
||||||
|
export const projects: Project[] = [
|
||||||
|
{
|
||||||
|
title: 'Fleet Management & Tracking',
|
||||||
|
blurb: 'Empower high-stakes healthcare environments with real-time tracking, telemetry, and remote management for EHR devices.',
|
||||||
|
stack: 'TypeScript · Next.js · SQL · Rust',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Leadership Training Platforms',
|
||||||
|
blurb: 'Prepare corporate leaders for critical decision-making through dynamic, real-world simulations of routine and high-stress management.',
|
||||||
|
stack: 'React · Tailwind CSS',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Battery Management Tooling',
|
||||||
|
blurb: 'Validated configuration tooling for programming power delivery controllers in multi-cell battery packs.',
|
||||||
|
stack: 'Next.js · Python · Embedded',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const feats: Feat[] = [
|
||||||
|
{
|
||||||
|
title: 'Live event production',
|
||||||
|
detail: 'Produced gatherings of up to 250 attendees and coordinated cross-country talent travel.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Community direction',
|
||||||
|
detail: "Led one of the Southern USA's largest interest-driven communities.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Media partnerships',
|
||||||
|
detail: 'Negotiated talent placements with global streaming networks.',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Public speaking',
|
||||||
|
detail: 'Regular convention panelist on community and operations leadership.',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -1,7 +1,13 @@
|
||||||
import type { NextConfig } from "next";
|
import type { NextConfig } from 'next';
|
||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
/* config options here */
|
async redirects() {
|
||||||
|
return [
|
||||||
|
{ source: '/blog', destination: '/', permanent: true },
|
||||||
|
{ source: '/blog/:slug*', destination: '/', permanent: true },
|
||||||
|
{ source: '/contact', destination: '/#contact', permanent: true },
|
||||||
|
];
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
28
package.json
28
package.json
|
|
@ -1,34 +1,30 @@
|
||||||
{
|
{
|
||||||
"name": "myblog",
|
"name": "caydenwebsite",
|
||||||
"version": "0.1.0",
|
"version": "2.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev",
|
"dev": "next dev",
|
||||||
"build": "next build",
|
"build": "next build",
|
||||||
"start": "next start",
|
"start": "next start",
|
||||||
"lint": "eslint"
|
"lint": "eslint",
|
||||||
|
"format": "prettier --write ."
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@heroicons/react": "^2.2.0",
|
"@heroicons/react": "^2.2.0",
|
||||||
"eslint-config-prettier": "^10.1.8",
|
"next": "^16.4.3",
|
||||||
"eslint-plugin-prettier": "^5.5.6",
|
"react": "^19.3.1",
|
||||||
"gray-matter": "^4.0.3",
|
"react-dom": "^19.3.1"
|
||||||
"moment": "^2.30.1",
|
|
||||||
"next": "16.2.12",
|
|
||||||
"prettier": "^3.9.4",
|
|
||||||
"react": "19.2.8",
|
|
||||||
"react-dom": "19.2.8",
|
|
||||||
"remark": "^15.0.1",
|
|
||||||
"remark-html": "^16.0.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/postcss": "^4.3.1",
|
"@tailwindcss/postcss": "^4.3.3",
|
||||||
"@types/node": "^20.19.43",
|
"@types/node": "^20.19.43",
|
||||||
"@types/react": "19.2.18",
|
"@types/react": "19.2.18",
|
||||||
"@types/react-dom": "19.2.4",
|
"@types/react-dom": "19.2.4",
|
||||||
"eslint": "10.8.0",
|
"eslint": "10.8.0",
|
||||||
"eslint-config-next": "16.2.12",
|
"eslint-config-next": "^16.4.3",
|
||||||
"tailwindcss": "^4.3.1",
|
"prettier": "^3.9.10",
|
||||||
|
"prettier-plugin-tailwindcss": "^0.8.1",
|
||||||
|
"tailwindcss": "^4.3.3",
|
||||||
"typescript": "^5.9.3"
|
"typescript": "^5.9.3"
|
||||||
},
|
},
|
||||||
"overrides": {
|
"overrides": {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
"@tailwindcss/postcss": {},
|
'@tailwindcss/postcss': {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
export type ArticleItem = {
|
|
||||||
id: string,
|
|
||||||
title: string,
|
|
||||||
date: string,
|
|
||||||
category: string
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue