feat: better navigation, updated cc.json, icons on homepage, better structure, cleanup manifesto page, NowPlaying.tsx fix

This commit is contained in:
Aidan 2025-09-01 05:08:38 -04:00
parent c1f0832f4a
commit f81b145bf7
9 changed files with 773 additions and 252 deletions

View file

@ -15,12 +15,6 @@ Docker is the easiest way to deploy aidxnCC. There are two example `docker-compo
Just create a `.env` file with the below variables, run `docker compose -d --build`, and you'll be all set. Just create a `.env` file with the below variables, run `docker compose -d --build`, and you'll be all set.
## Contributing
Any and all contributions are welcome! Simply create a pull request and I should have a response to you within a day.
Please use common sense when contributing :)
## Environment Variables ## Environment Variables
| Variable | Description | | Variable | Description |
@ -32,3 +26,9 @@ Please use common sense when contributing :)
This project does not use a custom user agent when interacting with the MusicBrainz API. This is because the LastPlayed component is rendered client-side and user agent support is not universal. This project does not use a custom user agent when interacting with the MusicBrainz API. This is because the LastPlayed component is rendered client-side and user agent support is not universal.
If bugs were to occur with my code, I believe it would be easier for MusicBrainz to block this way. If bugs were to occur with my code, I believe it would be easier for MusicBrainz to block this way.
## Contributing
Any and all contributions are welcome! Simply create a pull request and I should have a response to you within a day.
Please use common sense when contributing :)

View file

@ -4,6 +4,7 @@ import Header from '@/components/Header'
import Footer from '@/components/Footer' import Footer from '@/components/Footer'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { SiClaude } from 'react-icons/si' import { SiClaude } from 'react-icons/si'
import Link from 'next/link'
import { import {
Line, Line,
BarChart, BarChart,
@ -83,8 +84,103 @@ export default function AI() {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<Header /> <Header />
<main className="flex-1 flex items-center justify-center"> <main className="w-full relative">
<div className="text-gray-300">Loading Claude metrics...</div> <Link
href="/ai"
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
>
Back to AI
</Link>
<div className="my-12 text-center">
<div className="flex justify-center mb-6">
<SiClaude size={60} />
</div>
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">Claude Code Usage</h1>
<p className="text-gray-400">How much I use Claude Code!</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 px-4">
<div className="p-6 border-2 border-gray-700 rounded-lg">
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
</div>
<div className="p-6 border-2 border-gray-700 rounded-lg">
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
</div>
<div className="p-6 border-2 border-gray-700 rounded-lg">
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
</div>
<div className="p-6 border-2 border-gray-700 rounded-lg">
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
<section className="p-8 border-2 border-gray-700 rounded-lg">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Usage Trend</h2>
<div className="flex gap-2 mb-4">
<button className="px-3 py-1 rounded bg-gray-700 text-gray-300" disabled>
Cost
</button>
<button className="px-3 py-1 rounded bg-gray-700 text-gray-300" disabled>
Tokens
</button>
</div>
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
</section>
<section className="p-8 border-2 border-gray-700 rounded-lg">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Model Usage Distribution</h2>
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
</section>
<section className="p-8 border-2 border-gray-700 rounded-lg">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Type Breakdown</h2>
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
</section>
<section className="p-8 border-2 border-gray-700 rounded-lg">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Token Composition</h2>
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
</section>
</div>
<div className="px-4 pb-4">
<section className="p-8 border-2 border-gray-700 rounded-lg">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Recent Sessions</h2>
<div className="overflow-x-auto">
<table className="w-full text-left">
<thead>
<tr className="border-b border-gray-700">
<th className="py-2 px-4 text-gray-400">Date</th>
<th className="py-2 px-4 text-gray-400">Models Used</th>
<th className="py-2 px-4 text-gray-400">Total Tokens</th>
<th className="py-2 px-4 text-gray-400">Cost</th>
</tr>
</thead>
<tbody>
{[...Array(5)].map((_, index) => (
<tr key={index} className="border-b border-gray-800">
<td className="py-2 px-4">
<div className="h-5 w-24 bg-gray-800 rounded animate-pulse" />
</td>
<td className="py-2 px-4">
<div className="h-5 w-96 bg-gray-800 rounded animate-pulse" />
</td>
<td className="py-2 px-4">
<div className="h-5 w-16 bg-gray-800 rounded animate-pulse" />
</td>
<td className="py-2 px-4">
<div className="h-5 w-20 bg-gray-800 rounded animate-pulse" />
</td>
</tr>
))}
</tbody>
</table>
</div>
</section>
</div>
</main> </main>
<Footer /> <Footer />
</div> </div>
@ -114,6 +210,7 @@ export default function AI() {
}) })
return acc return acc
}, [] as { name: string; value: number }[]) }, [] as { name: string; value: number }[])
.sort((a, b) => b.value - a.value)
const tokenTypeData = [ const tokenTypeData = [
{ name: 'Input', value: data.totals.inputTokens }, { name: 'Input', value: data.totals.inputTokens },
@ -123,7 +220,7 @@ export default function AI() {
] ]
const dailyTrendData = data.daily.map(day => ({ const dailyTrendData = data.daily.map(day => ({
date: new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }), date: new Date(day.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
cost: day.totalCost, cost: day.totalCost,
tokens: day.totalTokens / 1000000, tokens: day.totalTokens / 1000000,
inputTokens: day.inputTokens / 1000, inputTokens: day.inputTokens / 1000,
@ -137,7 +234,13 @@ export default function AI() {
return ( return (
<div className="min-h-screen flex flex-col"> <div className="min-h-screen flex flex-col">
<Header /> <Header />
<main className="w-full"> <main className="w-full relative">
<Link
href="/ai"
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
>
Back to AI
</Link>
<div className="my-12 text-center"> <div className="my-12 text-center">
<div className="flex justify-center mb-6"> <div className="flex justify-center mb-6">
<SiClaude size={60} /> <SiClaude size={60} />
@ -227,6 +330,8 @@ export default function AI() {
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }} contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }}
formatter={(value: number) => formatCurrency(value)} formatter={(value: number) => formatCurrency(value)}
labelStyle={{ color: '#fff' }}
itemStyle={{ color: '#fff' }}
/> />
</PieChart> </PieChart>
</ResponsiveContainer> </ResponsiveContainer>
@ -273,10 +378,13 @@ export default function AI() {
<XAxis dataKey="name" stroke="#9ca3af" /> <XAxis dataKey="name" stroke="#9ca3af" />
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} /> <YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} />
<Tooltip <Tooltip
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }} contentStyle={{ backgroundColor: 'rgba(31, 41, 55)', border: '1px solid #374151' }}
formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`} formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`}
/> />
<Bar dataKey="value" fill="#b1ada1" /> <Bar
dataKey="value"
fill="#b1ada1"
/>
</BarChart> </BarChart>
</ResponsiveContainer> </ResponsiveContainer>
</section> </section>
@ -317,7 +425,7 @@ export default function AI() {
<tbody> <tbody>
{data.daily.slice(-5).reverse().map((day, index) => ( {data.daily.slice(-5).reverse().map((day, index) => (
<tr key={index} className="border-b border-gray-800 hover:bg-gray-800/50"> <tr key={index} className="border-b border-gray-800 hover:bg-gray-800/50">
<td className="py-2 px-4 text-gray-300">{new Date(day.date).toLocaleDateString()}</td> <td className="py-2 px-4 text-gray-300">{new Date(day.date + 'T00:00:00').toLocaleDateString()}</td>
<td className="py-2 px-4 text-gray-300"> <td className="py-2 px-4 text-gray-300">
{day.modelsUsed.join(', ')} {day.modelsUsed.join(', ')}
</td> </td>

View file

@ -16,7 +16,7 @@ export default function Manifesto() {
Internet Manifesto Internet Manifesto
</h1> </h1>
</div> </div>
<div className="px-6 pt-6"> <div className="px-6 pt-12">
<h2 className="text-2xl font-semibold mb-4 text-gray-200"> <h2 className="text-2xl font-semibold mb-4 text-gray-200">
1. Empathy and Understanding 1. Empathy and Understanding
</h2> </h2>
@ -28,6 +28,7 @@ export default function Manifesto() {
<li>Suspend judgment and seek to understand</li> <li>Suspend judgment and seek to understand</li>
<li>Recognize the humanity in every digital interaction</li> <li>Recognize the humanity in every digital interaction</li>
</ul> </ul>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200"> <h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
2. Unconditional Sharing! 2. Unconditional Sharing!
</h2> </h2>
@ -40,12 +41,14 @@ export default function Manifesto() {
<li>Support open-source principles</li> <li>Support open-source principles</li>
<li>Create extensive documentation on all of my projects</li> <li>Create extensive documentation on all of my projects</li>
</ul> </ul>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200"> <h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
3. Genuine Human Connection 3. Genuine Human Connection
</h2> </h2>
<p className="text-gray-300 mb-4"> <p className="text-gray-300 mb-4">
I aim to create a genuine human connection with all people I meet, regardless of who or where they are from. I aim to create a genuine human connection with all people I meet, regardless of who or where they are from.
</p> </p>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200"> <h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
4. Privacy & Self-Hosted Services 4. Privacy & Self-Hosted Services
</h2> </h2>
@ -62,6 +65,7 @@ export default function Manifesto() {
<li>Focus my services on being free and open</li> <li>Focus my services on being free and open</li>
<li>Suggest and support privacy-focused software</li> <li>Suggest and support privacy-focused software</li>
</ul> </ul>
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200"> <h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
I Commit I Commit
</h2> </h2>

View file

@ -4,11 +4,26 @@ import Header from '@/components/Header'
import Footer from '@/components/Footer' import Footer from '@/components/Footer'
import Button from '@/components/objects/Button' import Button from '@/components/objects/Button'
import LastPlayed from '@/components/widgets/NowPlaying' import LastPlayed from '@/components/widgets/NowPlaying'
import Image from 'next/image' import Image from 'next/image'
import { CreditCard, Mail, PillBottle, Scale } from 'lucide-react'
import {CreditCard, Mail, PillBottle, Scale, UserCircle} from 'lucide-react'
import { BsArrowClockwise } from "react-icons/bs";
import { FaHandcuffs } from "react-icons/fa6" import { FaHandcuffs } from "react-icons/fa6"
import {
SiGithubsponsors,
SiNextdotjs,
SiTailwindcss,
SiDocker,
SiLinux,
SiTypescript,
SiClaude,
SiPostgresql
} from 'react-icons/si'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { SiGithubsponsors } from 'react-icons/si' import {TbHeartHandshake, TbUserHeart, TbMessage} from "react-icons/tb";
import {BiDonateHeart} from "react-icons/bi";
export default function Home() { export default function Home() {
const { t } = useTranslation() const { t } = useTranslation()
@ -58,7 +73,33 @@ export default function Home() {
{mainSections.map((section, secIndex) => ( {mainSections.map((section, secIndex) => (
<section key={secIndex} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300"> <section key={secIndex} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{section}</h2> <h2 className="text-2xl font-semibold mb-4 text-gray-200">{section === t('home.sections.whereYouAre') ? (
<div className="flex flex-row items-center gap-2">
<TbHeartHandshake />
<span className="align-middle">{section}</span>
</div>
) : section === t('home.sections.whoIAm') ? (
<div className="flex flex-row items-center gap-2">
<UserCircle />
<span className="align-middle">{section}</span>
</div>
) : section === t('home.sections.whatIDo') ? (
<div className="flex flex-row items-center gap-2">
<TbUserHeart />
<span className="align-middle">{section}</span>
</div>
) : (section)}</h2>
{section === t('home.sections.whatIDo') && (
<div className="flex flex-row items-center justify-center gap-4 my-8">
<SiNextdotjs size={38} />
<SiTypescript size={38} />
<SiTailwindcss size={38} />
<SiPostgresql size={38} />
<SiDocker size={38} />
<SiLinux size={38} />
<SiClaude size={38} />
</div>
)}
{mainStrings[secIndex].map((text: string, index: number) => ( {mainStrings[secIndex].map((text: string, index: number) => (
<p key={index} className="text-gray-300 leading-relaxed mt-2"> <p key={index} className="text-gray-300 leading-relaxed mt-2">
{text} {text}
@ -68,7 +109,10 @@ export default function Home() {
))} ))}
<section id="contact" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300"> <section id="contact" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{t('home.contact.title')}</h2> <h2 className="flex flex-row items-center gap-2 text-2xl font-semibold mb-4 text-gray-200">
<TbMessage />
{t('home.contact.title')}
</h2>
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p> <p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
<Button <Button
href={'/contact'} href={'/contact'}
@ -79,7 +123,10 @@ export default function Home() {
</section> </section>
<section id="donation" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300"> <section id="donation" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{t('home.donation.title')}</h2> <h2 className="flex flex-row items-center gap-2 text-2xl font-semibold mb-4 text-gray-200">
<BiDonateHeart />
{t('home.donation.title')}
</h2>
<p className="text-gray-300 mb-6">{t('home.donation.description')}</p> <p className="text-gray-300 mb-6">{t('home.donation.description')}</p>
<h4 className="text-lg font-semibold mb-2 text-gray-200">{t('home.donation.charities.title')}</h4> <h4 className="text-lg font-semibold mb-2 text-gray-200">{t('home.donation.charities.title')}</h4>
<div className="grid grid-cols-1 md:grid-cols-2 md:text-sm gap-3"> <div className="grid grid-cols-1 md:grid-cols-2 md:text-sm gap-3">
@ -104,6 +151,13 @@ export default function Home() {
> >
{t('home.donation.charities.aclu')} {t('home.donation.charities.aclu')}
</Button> </Button>
<Button
href="https://www.epicrestartfoundation.org"
icon={<BsArrowClockwise size={16} />}
target="_blank"
>
{t('home.donation.charities.epic-restart')}
</Button>
</div> </div>
<h4 className="text-lg font-semibold mt-5 mb-2 text-gray-200">{t('home.donation.donate.title')}</h4> <h4 className="text-lg font-semibold mt-5 mb-2 text-gray-200">{t('home.donation.donate.title')}</h4>

View file

@ -12,8 +12,11 @@ import {
Menu, Menu,
Globe, Globe,
ChevronDown, ChevronDown,
Brain ChevronRight,
Brain,
Smartphone
} from 'lucide-react' } from 'lucide-react'
import { SiClaude, SiGoogle } from 'react-icons/si'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
interface NavItemProps { interface NavItemProps {
@ -31,6 +34,181 @@ const NavItem = ({ href, icon, children }: NavItemProps) => (
</div> </div>
); );
interface DropdownNavItemProps {
id: string;
href: string;
icon: React.ElementType;
children: React.ReactNode;
dropdownContent: React.ReactNode;
isMobile?: boolean;
isOpen?: boolean;
onOpenChange?: (id: string | null) => void;
}
const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile = false, isOpen = false, onOpenChange }: DropdownNavItemProps) => {
const dropdownRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
onOpenChange?.(null);
}
};
if (isMobile && isOpen) {
document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside);
}
}, [isMobile, isOpen, onOpenChange]);
const handleMouseEnter = () => {
if (!isMobile) {
onOpenChange?.(id);
}
};
const handleMouseLeave = (e: React.MouseEvent) => {
if (!isMobile) {
const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget && dropdownRef.current?.contains(relatedTarget)) {
return;
}
onOpenChange?.(null);
}
};
const handleClick = (e: React.MouseEvent) => {
if (isMobile) {
e.preventDefault();
onOpenChange?.(isOpen ? null : id);
}
};
return (
<div
className="nav-item relative"
ref={dropdownRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Link
href={href}
onClick={isMobile ? handleClick : undefined}
className="flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 w-full"
>
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
<span className="flex-1">{children}</span>
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
</Link>
{isOpen && (
<>
{/* Invisible bridge to handle gap */}
{!isMobile && (
<div className="absolute left-0 top-full w-full h-1 z-50" />
)}
<div
className={`${
isMobile
? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
: 'absolute left-0 mt-1 z-50 flex'
}`}
>
{dropdownContent}
</div>
</>
)}
</div>
);
};
interface NestedDropdownItemProps {
children: React.ReactNode;
nestedContent: React.ReactNode;
isMobile?: boolean;
}
const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: NestedDropdownItemProps) => {
const [isOpen, setIsOpen] = useState(false);
const itemRef = useRef<HTMLDivElement>(null);
const handleMouseEnter = () => {
if (!isMobile) {
setIsOpen(true);
}
};
const handleMouseLeave = (e: React.MouseEvent) => {
if (!isMobile) {
const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget && itemRef.current?.contains(relatedTarget)) {
return;
}
setIsOpen(false);
}
};
const handleClick = (e: React.MouseEvent) => {
if (isMobile) {
e.preventDefault();
setIsOpen(!isOpen);
}
};
if (isMobile) {
return (
<div
className="relative"
ref={itemRef}
>
<button
onClick={handleClick}
className="flex items-center justify-between w-full text-left px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300"
>
<span className="flex items-center">
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
{children}
</span>
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? '-rotate-90' : ''}`} strokeWidth={2.5} size={18} />
</button>
{isOpen && (
<div className="relative mt-2 ml-4 bg-gray-700/30 rounded-md">
{nestedContent}
</div>
)}
</div>
);
}
return (
<div
className="relative"
ref={itemRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button
onClick={handleClick}
className="flex items-center justify-between w-full text-left px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300"
>
<span className="flex items-center">
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
{children}
</span>
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? 'rotate-0' : 'rotate-90'}`} strokeWidth={2.5} size={18} />
</button>
{isOpen && (
<>
{/* Invisible bridge to handle gap */}
<div className="absolute left-full top-0 w-2 h-full z-50" />
<div className="absolute left-full top-0 ml-2 w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50">
{nestedContent}
</div>
</>
)}
</div>
);
};
const LanguageSelector = () => { const LanguageSelector = () => {
const { i18n } = useTranslation(); const { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -63,9 +241,33 @@ const LanguageSelector = () => {
} }
}; };
if (isMobile) {
document.addEventListener('mousedown', handleClickOutside); document.addEventListener('mousedown', handleClickOutside);
return () => document.removeEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside);
}, []); }
}, [isMobile]);
const handleMouseEnter = () => {
if (!isMobile) {
setIsOpen(true);
}
};
const handleMouseLeave = (e: React.MouseEvent) => {
if (!isMobile) {
const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget && dropdownRef.current?.contains(relatedTarget)) {
return;
}
setIsOpen(false);
}
};
const handleClick = () => {
if (isMobile) {
setIsOpen(!isOpen);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => { const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Escape') { if (e.key === 'Escape') {
@ -76,33 +278,35 @@ const LanguageSelector = () => {
} }
}; };
const buttonContent = (
<>
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
{languages.find(lang => lang.code === i18n.language)?.name || 'English'}
{!isMobile && (
<ChevronDown className={`w-4 h-4 ml-1 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={20} />
)}
</>
);
return ( return (
<div className="relative" ref={dropdownRef}> <div
className="relative"
ref={dropdownRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button <button
onClick={() => setIsOpen(!isOpen)} onClick={handleClick}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`} className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
aria-expanded={isOpen} aria-expanded={isOpen}
aria-haspopup="true" aria-haspopup="true"
> >
{buttonContent} <Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
<span className="flex-1">{languages.find(lang => lang.code === i18n.language)?.name || 'English'}</span>
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
</button> </button>
{isOpen && ( {isOpen && (
<>
{/* Invisible bridge to handle gap */}
{!isMobile && (
<div className="absolute right-0 top-full w-56 h-2 z-50" />
)}
<div <div
className={`${ className={`${
isMobile isMobile
? 'relative mt-1 w-full bg-gray-800 rounded-md shadow-lg' ? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
: 'absolute right-0 mt-2 w-48 bg-gray-800 rounded-md shadow-lg z-50' : 'absolute right-0 mt-2 w-56 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50'
}`} }`}
role="menu" role="menu"
aria-orientation="vertical" aria-orientation="vertical"
@ -112,17 +316,18 @@ const LanguageSelector = () => {
<button <button
key={lang.code} key={lang.code}
onClick={() => changeLanguage(lang.code)} onClick={() => changeLanguage(lang.code)}
className={`block w-full text-left px-4 py-2 text-sm ${ className={`block w-full text-left px-5 py-3 text-base rounded-md ${
i18n.language === lang.code i18n.language === lang.code
? 'text-white bg-gray-700' ? 'text-white bg-gray-700'
: 'text-gray-300 hover:text-white hover:bg-gray-700' : 'text-gray-300 hover:text-white hover:bg-gray-700'
}`} } transition-all duration-300`}
role="menuitem" role="menuitem"
> >
{lang.name} {lang.name}
</button> </button>
))} ))}
</div> </div>
</>
)} )}
</div> </div>
); );
@ -130,10 +335,75 @@ const LanguageSelector = () => {
export default function Header() { export default function Header() {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const toggleMenu = () => setIsOpen(!isOpen); const toggleMenu = () => setIsOpen(!isOpen);
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const aboutDropdownContent = (
<>
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
<Link href="/about" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
<User className="mr-3" strokeWidth={2.5} size={18} />
About Me
</Link>
<NestedDropdownItem
isMobile={isMobile}
nestedContent={
<>
<Link href="/device/bonito" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
<SiGoogle className="mr-3" size={18} />
Pixel 3a XL (bonito)
</Link>
<Link href="/device/cheetah" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
<SiGoogle className="mr-3" size={18} />
Pixel 7 Pro (cheetah)
</Link>
<Link href="/device/komodo" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
<SiGoogle className="mr-3" size={18} />
Pixel 9 Pro (komodo)
</Link>
</>
}
>
Devices
</NestedDropdownItem>
</div>
</>
);
const aiDropdownContent = (
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
<Link href="/ai" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
<Brain className="mr-3" strokeWidth={2.5} size={18} />
AI
</Link>
<Link href="/ai/claude" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
<SiClaude className="mr-3" size={18} />
Claude Usage
</Link>
</div>
);
return ( return (
<>
<div
className={`fixed inset-0 z-30 pointer-events-none transition-all duration-300 ${
activeDropdown && !isMobile
? 'backdrop-blur-sm opacity-100'
: 'backdrop-blur-none opacity-0'
}`}
/>
<header className="bg-gray-800 relative"> <header className="bg-gray-800 relative">
{isOpen && ( {isOpen && (
<div <div
@ -150,8 +420,28 @@ export default function Header() {
</button> </button>
<ul className={`flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-4 absolute lg:static bg-gray-800 lg:bg-transparent w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto p-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}> <ul className={`flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-4 absolute lg:static bg-gray-800 lg:bg-transparent w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto p-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
<NavItem href="/" icon={House}>Home</NavItem> <NavItem href="/" icon={House}>Home</NavItem>
<NavItem href="/about" icon={User}>About</NavItem> <DropdownNavItem
<NavItem href="/ai" icon={Brain}>AI</NavItem> id="about"
href="/about"
icon={User}
dropdownContent={aboutDropdownContent}
isMobile={isMobile}
isOpen={activeDropdown === 'about'}
onOpenChange={setActiveDropdown}
>
About
</DropdownNavItem>
<DropdownNavItem
id="ai"
href="/ai"
icon={Brain}
dropdownContent={aiDropdownContent}
isMobile={isMobile}
isOpen={activeDropdown === 'ai'}
onOpenChange={setActiveDropdown}
>
AI
</DropdownNavItem>
<NavItem href="/contact" icon={Phone}>Contact</NavItem> <NavItem href="/contact" icon={Phone}>Contact</NavItem>
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem> <NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem> <NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
@ -164,5 +454,6 @@ export default function Header() {
</div> </div>
</nav> </nav>
</header> </header>
</>
); );
} }

View file

@ -181,10 +181,10 @@ const NowPlaying: React.FC = () => {
rel="noopener noreferrer" rel="noopener noreferrer"
className="bg-gradient-to-b from-gray-700 to-gray-900 border-b border-gray-700 px-2 py-0 block" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}} className="bg-gradient-to-b from-gray-700 to-gray-900 border-b border-gray-700 px-2 py-0 block" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}
> >
<div className="text-center leading-none"> <div className="text-center leading-none pb-1">
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" className="-my-0.5" /> <ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" />
<ScrollTxt text={currentTrack.track_name} type="track" className="-my-0.5" /> <ScrollTxt text={currentTrack.track_name} type="track" className="-mt-0.5" />
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" className="-mt-1.5 mb-0.5" />} {currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" />}
</div> </div>
</a> </a>
{/* Album art */} {/* Album art */}
@ -202,70 +202,6 @@ const NowPlaying: React.FC = () => {
</div> </div>
)} )}
</div> </div>
{/* Player controls and seekbar */}
<div className="bg-gradient-to-b from-gray-700 to-gray-900 pb-2.5 flex flex-col items-center" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}>
<div className="flex justify-center items-center gap-0 px-2">
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
<defs>
<linearGradient id="skipBackGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f9fafb" />
<stop offset="49%" stopColor="#e5e7eb" />
<stop offset="51%" stopColor="#6b7280" />
<stop offset="100%" stopColor="#d1d5db" />
</linearGradient>
</defs>
<rect x="2" y="4" width="2" height="12" fill="url(#skipBackGradient)" />
<polygon points="12,4 6,10 12,16" fill="url(#skipBackGradient)" />
<polygon points="20,4 12,10 20,16" fill="url(#skipBackGradient)" />
</svg>
</button>
<div className="w-[1px] h-6 bg-gray-800 mx-0.5"></div>
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
<svg width="38" height="38" viewBox="0 0 24 24" className="drop-shadow-sm">
<defs>
<linearGradient id="pauseGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f9fafb" />
<stop offset="49%" stopColor="#e5e7eb" />
<stop offset="51%" stopColor="#6b7280" />
<stop offset="100%" stopColor="#d1d5db" />
</linearGradient>
</defs>
<rect x="6" y="4" width="4" height="16" fill="url(#pauseGradient)" />
<rect x="14" y="4" width="4" height="16" fill="url(#pauseGradient)" />
</svg>
</button>
<div className="w-[1px] h-6 bg-gray-800 mx-1"></div>
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
<defs>
<linearGradient id="skipForwardGradient" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stopColor="#f9fafb" />
<stop offset="49%" stopColor="#e5e7eb" />
<stop offset="51%" stopColor="#6b7280" />
<stop offset="100%" stopColor="#d1d5db" />
</linearGradient>
</defs>
<polygon points="2,4 9,10 2,16" fill="url(#skipForwardGradient)" />
<polygon points="9,4 17,10 9,16" fill="url(#skipForwardGradient)" />
<rect x="18" y="4" width="2" height="12" fill="url(#skipForwardGradient)" />
</svg>
</button>
</div>
<div className="relative w-full flex justify-center mt-1">
<div className="w-38 h-2 bg-gray-800 rounded-full relative">
<div className="absolute inset-0 bg-gradient-to-b from-white to-gray-600 rounded-full" style={{width: `${volume}%`}} />
<div
className="absolute top-1/2 transform -translate-y-1/2 w-3.5 h-3.5 bg-gradient-to-b from-gray-200 via-gray-300 to-gray-500 rounded-full border border-gray-400 shadow-inner" style={{
left: `calc(${volume}% - 8px)`,
backgroundImage: 'radial-gradient(circle at 30% 30%, #f0f0f0 0%, #c0c0c0 60%, #808080 100%), repeating-conic-gradient(#f9fafb 0deg 45deg, #9ca3af 45deg 90deg)',
backgroundBlendMode: 'overlay',
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 2px rgba(255,255,255,0.5)'
}}></div>
<input type="range" min="0" max="100" value={volume} onChange={(e) => setVolume(Number(e.target.value))} className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
</div>
</div>
</div>
</> </>
) )
} }

View file

@ -19,7 +19,7 @@
"next": "^15.5.2", "next": "^15.5.2",
"react": "^19.1.1", "react": "^19.1.1",
"react-dom": "^19.1.1", "react-dom": "^19.1.1",
"react-i18next": "^15.7.2", "react-i18next": "^15.7.3",
"react-icons": "^5.5.0", "react-icons": "^5.5.0",
"recharts": "^3.1.2", "recharts": "^3.1.2",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
@ -30,8 +30,8 @@
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.12", "@tailwindcss/postcss": "^4.1.12",
"@types/node": "^20.19.11", "@types/node": "^20.19.11",
"@types/react": "^19.1.11", "@types/react": "^19.1.12",
"@types/react-dom": "^19.1.8", "@types/react-dom": "^19.1.9",
"eslint": "^9.34.0", "eslint": "^9.34.0",
"eslint-config-next": "15.1.3", "eslint-config-next": "15.1.3",
"postcss": "^8.5.6", "postcss": "^8.5.6",

View file

@ -446,14 +446,142 @@
"cost": 8.052878999999999 "cost": 8.052878999999999
} }
] ]
},
{
"date": "2025-08-23",
"inputTokens": 114,
"outputTokens": 6030,
"cacheCreationTokens": 408902,
"cacheReadTokens": 2606990,
"totalTokens": 3022036,
"totalCost": 11.605633500000005,
"modelsUsed": [
"claude-opus-4-1-20250805",
"claude-sonnet-4-20250514"
],
"modelBreakdowns": [
{
"modelName": "claude-opus-4-1-20250805",
"inputTokens": 88,
"outputTokens": 5424,
"cacheCreationTokens": 387862,
"cacheReadTokens": 2545780,
"cost": 11.499202500000006
},
{
"modelName": "claude-sonnet-4-20250514",
"inputTokens": 26,
"outputTokens": 606,
"cacheCreationTokens": 21040,
"cacheReadTokens": 61210,
"cost": 0.106431
}
]
},
{
"date": "2025-08-26",
"inputTokens": 2836,
"outputTokens": 22779,
"cacheCreationTokens": 465292,
"cacheReadTokens": 11182259,
"totalTokens": 11673166,
"totalCost": 25.288227900000006,
"modelsUsed": [
"claude-opus-4-1-20250805",
"claude-sonnet-4-20250514"
],
"modelBreakdowns": [
{
"modelName": "claude-opus-4-1-20250805",
"inputTokens": 2745,
"outputTokens": 19221,
"cacheCreationTokens": 405641,
"cacheReadTokens": 10473081,
"cost": 24.798140250000003
},
{
"modelName": "claude-sonnet-4-20250514",
"inputTokens": 91,
"outputTokens": 3558,
"cacheCreationTokens": 59651,
"cacheReadTokens": 709178,
"cost": 0.49008765
}
]
},
{
"date": "2025-08-30",
"inputTokens": 151,
"outputTokens": 63263,
"cacheCreationTokens": 430727,
"cacheReadTokens": 4992045,
"totalTokens": 5486186,
"totalCost": 20.311188749999992,
"modelsUsed": [
"claude-opus-4-1-20250805"
],
"modelBreakdowns": [
{
"modelName": "claude-opus-4-1-20250805",
"inputTokens": 151,
"outputTokens": 63263,
"cacheCreationTokens": 430727,
"cacheReadTokens": 4992045,
"cost": 20.311188749999992
}
]
},
{
"date": "2025-08-31",
"inputTokens": 108,
"outputTokens": 777,
"cacheCreationTokens": 40539,
"cacheReadTokens": 305195,
"totalTokens": 346619,
"totalCost": 1.2777937499999998,
"modelsUsed": [
"claude-opus-4-1-20250805"
],
"modelBreakdowns": [
{
"modelName": "claude-opus-4-1-20250805",
"inputTokens": 108,
"outputTokens": 777,
"cacheCreationTokens": 40539,
"cacheReadTokens": 305195,
"cost": 1.2777937499999998
}
]
},
{
"date": "2025-09-01",
"inputTokens": 592,
"outputTokens": 28240,
"cacheCreationTokens": 712734,
"cacheReadTokens": 12698327,
"totalTokens": 13439893,
"totalCost": 34.53813299999999,
"modelsUsed": [
"claude-opus-4-1-20250805"
],
"modelBreakdowns": [
{
"modelName": "claude-opus-4-1-20250805",
"inputTokens": 592,
"outputTokens": 28240,
"cacheCreationTokens": 712734,
"cacheReadTokens": 12698327,
"cost": 34.53813299999999
}
]
} }
], ],
"totals": { "totals": {
"inputTokens": 206310, "inputTokens": 210111,
"outputTokens": 1126501, "outputTokens": 1247590,
"cacheCreationTokens": 23364621, "cacheCreationTokens": 25422815,
"cacheReadTokens": 491053910, "cacheReadTokens": 522838726,
"totalCost": 617.5983253500001, "totalCost": 710.6193022500001,
"totalTokens": 515751342 "totalTokens": 549719242
} }
} }

View file

@ -1,10 +1,10 @@
{ {
"home": { "home": {
"whoAmI": [ "whoAmI": [
"Hey there! My name is Aidan, and I'm a systems administrator, full-stack developer, and student from the United States. I primarily work with Linux, Docker, Next.js, Tailwind CSS and TypeScript.", "Hey there! My name is Aidan, and I'm a systems administrator, full-stack developer, and student from the Boston area. I primarily work with Linux, Docker, Next.js, Tailwind CSS and TypeScript.",
"My favorite projects and hobbies include web development and SysAdmin. Most of my work is released into the public domain.", "My favorite projects and hobbies revolve around web development and SysAdmin. Most of my work is released into the public domain.",
"I'm also a huge advocate for AI, and it's practical applications to programming and life itself. I am fond of open-source models the most, specifically Qwen3!", "I'm also a huge advocate for AI and it's practical applications to programming and life itself. I am fond of open-source models the most, specifically Qwen3!",
"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and jumping between projects." "When I'm not programming, I can be found re-flashing my phone with a new custom ROM and jumping between projects. I tend to be quite depressed, but I make do."
], ],
"whatIDo": [ "whatIDo": [
"I'm at my best when I'm doing system administration and development in TypeScript. I frequently implement AI into my workflow.", "I'm at my best when I'm doing system administration and development in TypeScript. I frequently implement AI into my workflow.",
@ -13,7 +13,7 @@
], ],
"whereYouAre": [ "whereYouAre": [
"I am not here to brag about my accomplishments or plug my shitty SaaS. That's why I've made every effort to make this website as personal and fun as possible.", "I am not here to brag about my accomplishments or plug my shitty SaaS. That's why I've made every effort to make this website as personal and fun as possible.",
"I hope you find this website an interesting place to find more about me, but also learn something new, and inspire a new project or two.", "I hope you find this website an interesting place to find more about me, but also learn something new; maybe inspire a new project or two.",
"In a technical sense, this site is hosted on my dedicated server hosted in Buffalo, New York by ColoCrossing." "In a technical sense, this site is hosted on my dedicated server hosted in Buffalo, New York by ColoCrossing."
], ],
"sections": { "sections": {
@ -31,10 +31,10 @@
"description": "Feeling generous? Support me or one of the causes I support!", "description": "Feeling generous? Support me or one of the causes I support!",
"charities": { "charities": {
"title": "Charities", "title": "Charities",
"description": "I support the following charities:",
"unsilenced": "Unsilenced", "unsilenced": "Unsilenced",
"drugpolicy": "Drug Policy Alliance", "drugpolicy": "Drug Policy Alliance",
"aclu": "ACLU" "aclu": "ACLU",
"epic-restart": "EPIC Restart Foundation"
}, },
"donate": { "donate": {
"title": "Donate to Me", "title": "Donate to Me",