feat (v1.0.0): initial refactor and redesign

This commit is contained in:
Aidan 2025-10-09 04:12:05 -04:00
parent 3058aa1ab4
commit fe9b50b30e
134 changed files with 17792 additions and 3670 deletions

View file

@ -1,21 +0,0 @@
import { TbCopyrightOff } from "react-icons/tb"
import { RxDividerVertical } from "react-icons/rx"
import Link from 'next/link'
import RandomFooterMsg from "./objects/RandomFooterMsg"
export default function Footer() {
return (
<footer className="bg-gray-800 text-gray-400 py-4">
<div className="flex flex-col sm:flex-row container mx-auto px-4 text-center items-center justify-center">
<Link href="https://git.p0ntus.com/aidan/aidxnCC" target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
<div className="flex items-center justify-center">
<TbCopyrightOff className="text-md mr-2" />
Open Source and Copyright-Free
</div>
</Link>
<RxDividerVertical className="hidden sm:block mx-4"/>
<RandomFooterMsg />
</div>
</footer>
)
}

View file

@ -1,473 +0,0 @@
"use client"
import React, { useState, useRef, useEffect } from 'react'
import Link from 'next/link'
import {
House,
Link as LinkIcon,
User,
Phone,
BookOpen,
X,
Menu,
Globe,
ChevronDown,
ChevronRight,
Brain,
Smartphone
} from 'lucide-react'
import { TbUserHeart } from 'react-icons/tb'
import { SiClaude, SiGoogle } from 'react-icons/si'
import { useTranslation } from 'react-i18next'
interface NavItemProps {
href: string;
icon: React.ElementType;
children: React.ReactNode;
}
const NavItem = ({ href, icon, children }: NavItemProps) => (
<div className="nav-item">
<Link href={href} className="flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300">
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
{children}
</Link>
</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('click', handleClickOutside);
return () => document.removeEventListener('click', 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();
e.stopPropagation();
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 justify-between text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 w-full"
>
<span className="flex items-center flex-1">
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
<span>{children}</span>
</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 w-full mt-2 ml-5 pr-4'
: '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 timeoutRef = useRef<NodeJS.Timeout | null>(null);
const handleMouseEnter = () => {
if (!isMobile) {
if (timeoutRef.current) clearTimeout(timeoutRef.current);
setIsOpen(true);
}
};
const handleMouseLeave = (e: React.MouseEvent) => {
if (!isMobile) {
const relatedTarget = e.relatedTarget as HTMLElement;
if (relatedTarget && itemRef.current?.contains(relatedTarget)) {
return;
}
timeoutRef.current = setTimeout(() => setIsOpen(false), 100);
}
};
const handleClick = (e: React.MouseEvent) => {
if (isMobile) {
e.preventDefault();
e.stopPropagation();
setIsOpen(!isOpen);
}
};
if (isMobile) {
return (
<div
className="relative"
ref={itemRef}
>
<button
onClick={handleClick}
className="flex items-center justify-between w-full text-left px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300"
>
<span className="flex items-center flex-1">
<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-5 pr-4 space-y-1">
{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-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300"
>
<span className="flex items-center flex-1">
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
{children}
</span>
<ChevronDown className={`transform transition-transform duration-200 ${isOpen ? '-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 { i18n } = useTranslation();
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const dropdownRef = useRef<HTMLDivElement>(null);
const languages = [
{ code: 'en-US', name: 'English' },
];
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const changeLanguage = async (lng: string) => {
await i18n.changeLanguage(lng);
setIsOpen(false);
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
if (isMobile) {
document.addEventListener('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) => {
if (e.key === 'Escape') {
setIsOpen(false);
}
if (e.key === 'Enter' || e.key === ' ') {
setIsOpen(!isOpen);
}
};
return (
<div
className="relative"
ref={dropdownRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button
onClick={handleClick}
onKeyDown={handleKeyDown}
className={`flex items-center ${isMobile ? 'justify-between' : ''} 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-haspopup="true"
>
<span className="flex items-center flex-1">
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
<span>{languages.find(lang => lang.code === i18n.language)?.name || 'English'}</span>
</span>
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
</button>
{isOpen && (
<>
{/* Invisible bridge to handle gap */}
{!isMobile && (
<div className="absolute right-0 top-full w-56 h-2 z-50" />
)}
<div
className={`${
isMobile
? 'relative w-full mt-2 ml-4 pr-4 space-y-1'
: 'absolute right-0 mt-2 w-56 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50'
}`}
role="menu"
aria-orientation="vertical"
aria-labelledby="language-menu"
>
{languages.map((lang) => (
<button
key={lang.code}
onClick={() => changeLanguage(lang.code)}
className={`block w-full text-left ${isMobile ? 'px-4 py-2.5' : 'px-5 py-3'} ${isMobile ? 'text-sm' : 'text-base'} rounded-md ${
i18n.language === lang.code
? 'text-white bg-gray-700/50'
: 'text-gray-300 hover:text-white hover:bg-gray-700/50'
} transition-all duration-300`}
role="menuitem"
>
{lang.name}
</button>
))}
</div>
</>
)}
</div>
);
};
export default function Header() {
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const toggleMenu = () => {
setIsOpen(!isOpen);
if (isOpen) {
setActiveDropdown(null);
}
};
const handleDropdownChange = (id: string | null) => {
setActiveDropdown(id);
};
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
const aboutDropdownContent = (
<>
<div className={`${isMobile ? 'w-full' : 'w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700'}`}>
<Link href="/about" className={`flex items-center px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300 cursor-pointer`}>
<TbUserHeart className="mr-3" size={18} />
Get to Know Me
</Link>
<NestedDropdownItem
isMobile={isMobile}
nestedContent={
<>
<Link href="/device/bonito" className={`flex items-center ${isMobile ? 'px-4 py-3' : 'px-5 py-3'} text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 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 ${isMobile ? 'px-4 py-3' : 'px-5 py-3'} text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 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 ${isMobile ? 'px-4 py-3' : 'px-5 py-3'} text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300`}>
<SiGoogle className="mr-3" size={18} />
Pixel 9 Pro (komodo)
</Link>
</>
}
>
Devices
</NestedDropdownItem>
</div>
</>
);
const aiDropdownContent = (
<div className={`${isMobile ? 'w-full' : 'w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700'}`}>
<Link href="/ai/claude" className={`flex items-center px-4 py-3 text-sm text-gray-300 hover:text-white hover:bg-gray-700/30 rounded-md transition-all duration-300`}>
<SiClaude className="mr-3" size={18} />
Claude Usage
</Link>
</div>
);
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">
{isOpen && (
<div
className="fixed inset-0 backdrop-blur-md z-40 lg:hidden"
onClick={toggleMenu}
/>
)}
<nav className="container mx-auto px-4 py-4 flex justify-between items-center relative z-50">
<Link href="/" className="text-gray-300 hover:text-white text-2xl font-bold transition-all duration-300 hover:glow">
aidxn.cc
</Link>
<button onClick={toggleMenu} className="lg:hidden text-gray-300 focus:outline-hidden">
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
</button>
<ul className={`flex flex-col lg:flex-row space-y-3 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 px-2 py-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
<NavItem href="/" icon={House}>Home</NavItem>
<DropdownNavItem
id="about"
href="/about"
icon={User}
dropdownContent={aboutDropdownContent}
isMobile={isMobile}
isOpen={activeDropdown === 'about'}
onOpenChange={handleDropdownChange}
>
About Me
</DropdownNavItem>
<DropdownNavItem
id="ai"
href="/ai"
icon={Brain}
dropdownContent={aiDropdownContent}
isMobile={isMobile}
isOpen={activeDropdown === 'ai'}
onOpenChange={handleDropdownChange}
>
AI
</DropdownNavItem>
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
<div className="lg:hidden mt-2 pt-3 -mb-1.5 border-t border-gray-600/30">
<LanguageSelector />
</div>
</ul>
<div className="hidden lg:block">
<LanguageSelector />
</div>
</nav>
</header>
</>
);
}

View file

@ -1,8 +0,0 @@
"use client";
import { ReactNode } from "react";
import "../i18n";
export default function I18nProvider({ children }: { children: ReactNode }) {
return <>{children}</>;
}

View file

@ -0,0 +1,81 @@
import Image from 'next/image';
import type { DeviceHeroProps } from '@/lib/types';
import { deviceTypeLabels } from '@/lib/devices/config';
export default function DeviceHero({ device }: DeviceHeroProps) {
const imageWidth = device.heroImage.width ?? 540;
const imageHeight = device.heroImage.height ?? 540;
const metadata = [
{
label: 'Type',
value: deviceTypeLabels[device.type],
},
device.releaseYear
? {
label: 'Release',
value: device.releaseYear.toString(),
}
: undefined,
device.status
? {
label: 'Status',
value: device.status,
}
: undefined,
device.codename
? {
label: 'Codename',
value: device.codename,
}
: undefined,
].filter(Boolean) as Array<{ label: string; value: string }>;
return (
<section className="grid grid-cols-1 xl:grid-cols-[minmax(0,1.2fr)_minmax(0,1fr)] gap-8 xl:gap-12">
<div className="bg-gray-900/60 border border-gray-800 rounded-2xl p-6 md:p-8 backdrop-blur-sm space-y-6">
<div className="space-y-3">
<h1 className="text-3xl md:text-4xl font-semibold text-gray-100">
{device.name}
</h1>
{device.tagline ? (
<p className="text-base md:text-lg text-gray-400 max-w-2xl">{device.tagline}</p>
) : null}
</div>
{device.summary?.length ? (
<div className="space-y-3 text-sm md:text-base text-gray-400 leading-relaxed max-w-2xl">
{device.summary.map((paragraph, idx) => (
<p key={idx}>{paragraph}</p>
))}
</div>
) : null}
{metadata.length ? (
<dl className="grid gap-3 sm:grid-cols-2 lg:grid-cols-3 text-sm text-gray-400">
{metadata.map((item) => (
<div key={item.label} className="flex flex-col">
<dt className="uppercase text-xs tracking-wide text-gray-600">{item.label}</dt>
<dd className="font-medium text-gray-200">{item.value}</dd>
</div>
))}
</dl>
) : null}
</div>
<div className="flex items-center justify-center">
<div className="w-full max-w-md rounded-2xl border border-gray-800 bg-gray-900/60 p-6 md:p-8 flex items-center justify-center">
<Image
src={device.heroImage.src}
alt={device.heroImage.alt}
width={imageWidth}
height={imageHeight}
className="w-full h-auto object-contain drop-shadow-lg"
priority
/>
</div>
</div>
</section>
);
}

View file

@ -0,0 +1,283 @@
import type { ReactElement } from 'react';
import { ArrowUpRight, Star, StarHalf, StarOff } from 'lucide-react';
import Link from '@/components/objects/Link';
import type {
DevicePageShellProps,
DeviceStatGroup,
StatsGridProps,
StatItemProps,
SectionsGridProps,
SectionCardProps,
SectionRowProps,
RatingProps,
StarState,
} from '@/lib/types';
import { isExternalHref, externalLinkProps } from '@/lib/utils/styles';
import { iconSizes } from '@/lib/devices/config';
import DeviceHero from './DeviceHero';
export default function DevicePageShell({ device }: DevicePageShellProps): ReactElement {
return (
<div className="space-y-12">
<DeviceHero device={device} />
{device.stats.length ? <StatsGrid stats={device.stats} /> : null}
{device.sections.length ? <SectionsGrid sections={device.sections} /> : null}
</div>
);
}
function StatsGrid({ stats }: StatsGridProps): ReactElement {
return (
<section className="space-y-5">
<h2 className="text-xl font-semibold text-gray-100">At a glance</h2>
<div className="grid gap-5 lg:grid-cols-2 xl:grid-cols-3 auto-rows-fr">
{stats.map((group) => (
<StatCard key={group.title} group={group} />
))}
</div>
</section>
);
}
function StatCard({ group }: { group: DeviceStatGroup }): ReactElement {
const Icon = group.icon;
return (
<article className="flex h-full flex-col gap-4 rounded-2xl border border-gray-800 bg-gray-900/60 p-5 backdrop-blur-sm">
<header className="flex items-center gap-3">
{Icon ? (
<span className="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gray-800 text-gray-300">
<Icon className="h-5 w-5" />
</span>
) : null}
<h3 className="text-lg font-semibold text-gray-100">{group.title}</h3>
</header>
<div className="grid gap-3 sm:grid-cols-2">
{group.items.map((item) => (
<StatItem
key={`${group.title}-${item.label ?? item.value}`}
item={item}
groupIcon={group.icon}
/>
))}
</div>
</article>
);
}
function StatItem({ item, groupIcon }: StatItemProps): ReactElement {
const isExternal = isExternalHref(item.href);
const linkProps = isExternal ? externalLinkProps : {};
const baseClasses =
'relative overflow-hidden rounded-2xl border border-gray-800 bg-gray-900/70 px-4 py-5 text-gray-100 transition';
const GroupIcon = groupIcon;
const content = (
<>
{GroupIcon ? (
<GroupIcon
aria-hidden
className="pointer-events-none absolute -top-4 -right-4 text-gray-800/70"
size={iconSizes.stat}
/>
) : null}
{item.href && isExternal ? (
<ArrowUpRight
aria-hidden
className="pointer-events-none absolute bottom-4 right-4 z-20 text-gray-500"
/>
) : null}
<div className="relative z-10 space-y-2 pr-10">
{item.label ? (
<p className="text-xs uppercase tracking-wide text-gray-500">{item.label}</p>
) : null}
<div className="text-lg font-semibold leading-snug text-gray-100">{item.value}</div>
</div>
</>
);
if (item.href) {
return (
<Link
href={item.href}
className={`${baseClasses} block hover:text-white hover:no-underline`}
{...linkProps}
>
{content}
</Link>
);
}
return <div className={baseClasses}>{content}</div>;
}
function SectionsGrid({ sections }: SectionsGridProps): ReactElement {
return (
<section className="space-y-5">
<h2 className="text-xl font-semibold text-gray-100">Deep dive</h2>
<div className="grid gap-5 lg:grid-cols-2 xl:grid-cols-3 auto-rows-fr">
{sections.map((section) => (
<SectionCard key={section.id} section={section} />
))}
</div>
</section>
);
}
function SectionCard({ section }: SectionCardProps): ReactElement {
const Icon = section.icon;
const shouldSpanWide =
!!section.paragraphs?.length && (!section.rows || section.paragraphs.length > 1);
return (
<article
className={`rounded-2xl border border-gray-800 bg-gray-900/60 p-5 backdrop-blur-sm flex flex-col gap-4 ${
shouldSpanWide ? 'lg:col-span-2 xl:col-span-2' : ''
}`}
>
<header className="flex items-center gap-3">
<span className="inline-flex h-9 w-9 items-center justify-center rounded-xl bg-gray-800 text-gray-300">
<Icon className="h-5 w-5" />
</span>
<div>
<h3 className="text-lg font-semibold text-gray-100">{section.title}</h3>
{section.rating ? <Rating rating={section.rating} /> : null}
</div>
</header>
{section.rows?.length ? (
<div className="grid gap-3 sm:grid-cols-2">
{section.rows.map((row) => (
<SectionRow key={row.label} row={row} />
))}
</div>
) : null}
{section.listItems?.length ? (
<ul className="grid gap-2 text-sm text-gray-300">
{section.listItems.map((item) => {
const isExternal = isExternalHref(item.href);
const linkProps = isExternal ? externalLinkProps : {};
return (
<li key={item.label}>
{item.href ? (
<Link
href={item.href}
className="relative block rounded-xl border border-gray-800 bg-gray-900/70 px-3 py-2 text-gray-100 transition hover:text-white hover:no-underline"
{...linkProps}
>
<span className="block pr-10 font-medium">{item.label}</span>
{isExternal ? (
<ArrowUpRight
aria-hidden
className="pointer-events-none absolute bottom-2.5 right-3 text-gray-500"
/>
) : null}
</Link>
) : (
<div className="rounded-xl border border-gray-800 bg-gray-900/70 px-3 py-2 text-gray-100">
<span className="font-medium">{item.label}</span>
</div>
)}
{item.description ? (
<p className="mt-1 text-xs text-gray-500">{item.description}</p>
) : null}
</li>
);
})}
</ul>
) : null}
{section.paragraphs?.length ? (
<div className="space-y-3 text-sm leading-relaxed text-gray-400">
{section.paragraphs.map((paragraph) => (
<p key={`${section.id}-${paragraph}`}>{paragraph}</p>
))}
</div>
) : null}
</article>
);
}
function SectionRow({ row }: SectionRowProps): ReactElement {
const { icon: RowIcon } = row;
const isExternal = isExternalHref(row.href);
const linkProps = isExternal ? externalLinkProps : {};
const baseClasses =
'relative overflow-hidden rounded-2xl border border-gray-800 bg-gray-900/70 px-4 py-5 text-gray-100 transition';
const content = (
<>
{RowIcon ? (
<RowIcon className="pointer-events-none absolute -top-4 -right-4 text-gray-800/70" size={iconSizes.section} />
) : null}
{row.href && isExternal ? (
<ArrowUpRight
aria-hidden
className="pointer-events-none absolute bottom-4 right-4 z-20 h-4 w-4 text-gray-500"
/>
) : null}
<div className="relative z-10 space-y-2 pr-10">
<p className="text-xs uppercase tracking-wide text-gray-500">{row.label}</p>
<div className="text-lg font-semibold leading-snug text-gray-100">{row.value}</div>
{row.note ? <p className="text-xs text-gray-500">{row.note}</p> : null}
</div>
</>
);
if (row.href) {
return (
<Link
href={row.href}
className={`${baseClasses} block hover:text-white hover:no-underline`}
{...linkProps}
>
{content}
</Link>
);
}
return <div className={baseClasses}>{content}</div>;
}
function Rating({ rating }: RatingProps): ReactElement {
const stars = buildStars(rating.value, rating.scale ?? 5);
return (
<div className="mt-1 flex items-center gap-2 text-sm text-gray-400">
<span className="flex items-center text-gray-200">
{stars.map((state, idx) => {
const key = `${rating.label ?? rating.value}-${idx}`;
if (state === 'full') {
return <Star key={key} className="fill-current" />;
}
if (state === 'half') {
return <StarHalf key={key} className="fill-current" />;
}
return <StarOff key={key} className="text-gray-600" />;
})}
</span>
<span className="text-gray-300">{rating.value.toFixed(1)}</span>
{rating.label ? <span className="text-xs uppercase tracking-wide text-gray-600">{rating.label}</span> : null}
</div>
);
}
function buildStars(value: number, scale: number): StarState[] {
const stars: StarState[] = [];
const normalized = Math.max(0, Math.min(value, scale));
for (let i = 1; i <= scale; i += 1) {
if (normalized >= i) {
stars.push('full');
} else if (normalized > i - 1 && normalized < i) {
stars.push('half');
} else {
stars.push('empty');
}
}
return stars;
}

View file

@ -0,0 +1,257 @@
import { cn } from '@/lib/utils'
import { colors } from '@/lib/theme'
import type { APIEndpoint } from '@/lib/docs/types'
import CodeBlock from './CodeBlock'
import { LuLock } from 'react-icons/lu'
interface APIEndpointDocProps {
endpoint: APIEndpoint
className?: string
}
const methodStyles = {
GET: {
backgroundColor: 'rgba(16, 185, 129, 0.1)',
color: colors.accents.success,
borderColor: 'rgba(16, 185, 129, 0.3)',
},
POST: {
backgroundColor: 'rgba(59, 130, 246, 0.1)',
color: colors.accents.info,
borderColor: 'rgba(59, 130, 246, 0.3)',
},
PUT: {
backgroundColor: colors.accents.warningBg,
color: colors.accents.warning,
borderColor: 'rgba(245, 158, 11, 0.3)',
},
DELETE: {
backgroundColor: 'rgba(239, 68, 68, 0.1)',
color: colors.accents.error,
borderColor: 'rgba(239, 68, 68, 0.3)',
},
PATCH: {
backgroundColor: 'rgba(168, 85, 247, 0.1)',
color: '#a855f7',
borderColor: 'rgba(168, 85, 247, 0.3)',
},
} as const
export default function APIEndpointDoc({
endpoint,
className,
}: APIEndpointDocProps) {
return (
<div id={endpoint.id} className={cn('scroll-mt-20', className)}>
<div className="space-y-6">
{/* Header */}
<div className="space-y-3">
<div className="flex items-center gap-3">
<span
className="rounded-md border px-3 py-1 text-sm font-bold"
style={methodStyles[endpoint.method]}
>
{endpoint.method}
</span>
<code className="text-lg font-mono" style={{ color: colors.text.secondary }}>
{endpoint.path}
</code>
</div>
<p className="leading-relaxed" style={{ color: colors.text.body }}>{endpoint.description}</p>
{endpoint.auth?.required && (
<div
className="flex items-center gap-2 rounded-lg border px-4 py-2 text-sm"
style={{
borderColor: 'rgba(245, 158, 11, 0.3)',
backgroundColor: colors.accents.warningBg,
color: colors.accents.warning,
}}
>
<LuLock className="h-4 w-4" />
<span>
Authentication required
{endpoint.auth.type && `: ${endpoint.auth.type}`}
</span>
</div>
)}
</div>
{/* Query Parameters */}
{endpoint.parameters?.query && endpoint.parameters.query.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>
Query Parameters
</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b" style={{ borderColor: colors.borders.default }}>
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
Name
</th>
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
Type
</th>
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
Description
</th>
</tr>
</thead>
<tbody>
{endpoint.parameters.query.map((param, index) => (
<tr
key={index}
className="border-b last:border-0"
style={{ borderColor: colors.borders.subtle }}
>
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
{param.name}
{!param.optional && (
<span className="ml-1" style={{ color: colors.accents.error }}>*</span>
)}
</td>
<td className="px-4 py-3 font-mono" style={{ color: colors.text.muted }}>
{param.type}
</td>
<td className="px-4 py-3" style={{ color: colors.text.body }}>
{param.description}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Request Body */}
{endpoint.parameters?.body && endpoint.parameters.body.length > 0 && (
<div className="space-y-2">
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>Request Body</h4>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b" style={{ borderColor: colors.borders.default }}>
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
Field
</th>
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
Type
</th>
<th className="px-4 py-2 text-left font-medium" style={{ color: colors.text.muted }}>
Description
</th>
</tr>
</thead>
<tbody>
{endpoint.parameters.body.map((param, index) => (
<tr
key={index}
className="border-b last:border-0"
style={{ borderColor: colors.borders.subtle }}
>
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
{param.name}
{!param.optional && (
<span className="ml-1" style={{ color: colors.accents.error }}>*</span>
)}
</td>
<td className="px-4 py-3 font-mono" style={{ color: colors.text.muted }}>
{param.type}
</td>
<td className="px-4 py-3" style={{ color: colors.text.body }}>
{param.description}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Responses */}
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>Responses</h4>
{endpoint.responses.map((response, index) => {
const isSuccess = response.status >= 200 && response.status < 300
const isError = response.status >= 400
const statusStyle = isSuccess
? { backgroundColor: 'rgba(16, 185, 129, 0.1)', color: colors.accents.success }
: isError
? { backgroundColor: 'rgba(239, 68, 68, 0.1)', color: colors.accents.error }
: { backgroundColor: 'rgba(59, 130, 246, 0.1)', color: colors.accents.info }
return (
<div
key={index}
className="space-y-2 rounded-lg border p-4"
style={{
borderColor: colors.borders.default,
backgroundColor: colors.backgrounds.card,
}}
>
<div className="flex items-center gap-3">
<span
className="rounded px-2 py-1 text-sm font-mono font-semibold"
style={statusStyle}
>
{response.status}
</span>
<span className="text-sm" style={{ color: colors.text.body }}>
{response.description}
</span>
</div>
{response.example && (
<CodeBlock
code={JSON.stringify(response.example, null, 2)}
language="json"
title="Example Response"
/>
)}
</div>
)
})}
</div>
{/* Examples */}
{endpoint.examples && endpoint.examples.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.body }}>
Request Examples
</h4>
{endpoint.examples.map((example, index) => (
<div key={index} className="space-y-3">
{example.title && (
<h5 className="text-sm font-medium" style={{ color: colors.text.muted }}>
{example.title}
</h5>
)}
<div className="grid gap-3 lg:grid-cols-2">
<CodeBlock
code={
typeof example.request === 'string'
? example.request
: JSON.stringify(example.request, null, 2)
}
language="bash"
title="Request"
/>
<CodeBlock
code={
typeof example.response === 'string'
? example.response
: JSON.stringify(example.response, null, 2)
}
language="json"
title="Response"
/>
</div>
</div>
))}
</div>
)}
</div>
</div>
)
}

View file

@ -0,0 +1,198 @@
'use client'
import { useState } from 'react'
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter'
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism'
import { cn } from '@/lib/utils'
import { colors, effects } from '@/lib/theme'
import { Copy, Check } from 'lucide-react'
/**
* Supported syntax highlighting languages for code blocks.
*
* @remarks
* This list includes the most commonly used languages in the codebase.
* Languages are validated and normalized to ensure proper syntax highlighting.
*/
const SUPPORTED_LANGUAGES = [
'typescript',
'javascript',
'tsx',
'jsx',
'ts',
'js',
'json',
'bash',
'shell',
'css',
'scss',
'html',
'markdown',
'yaml',
'sql',
] as const
type SupportedLanguage = typeof SUPPORTED_LANGUAGES[number]
/**
* Normalizes language identifiers to their canonical forms.
*
* @param language - Raw language identifier from code fence
* @returns Normalized language identifier for syntax highlighting
*
* @remarks
* **Normalization rules:**
* - 'ts' 'typescript'
* - 'js' 'javascript'
* - Invalid languages 'typescript' (safe default)
* - All other valid languages unchanged
*
* This ensures consistent syntax highlighting even when JSDoc
* examples use shorthand language identifiers.
*
* @example
* ```ts
* normalizeLanguage('ts') // Returns: 'typescript'
* normalizeLanguage('tsx') // Returns: 'tsx'
* normalizeLanguage('invalid') // Returns: 'typescript'
* ```
*
* @private
*/
function normalizeLanguage(language: string): SupportedLanguage {
const normalized = language.toLowerCase()
// Map common shorthands to full names
if (normalized === 'ts') return 'typescript'
if (normalized === 'js') return 'javascript'
// Validate against supported languages
if (SUPPORTED_LANGUAGES.includes(normalized as SupportedLanguage)) {
return normalized as SupportedLanguage
}
// Default to typescript for unknown languages
return 'typescript'
}
interface CodeBlockProps {
code: string
language?: string
title?: string
showLineNumbers?: boolean
className?: string
}
export default function CodeBlock({
code,
language = 'typescript',
title,
showLineNumbers = false,
className,
}: CodeBlockProps) {
const [copied, setCopied] = useState(false)
const normalizedLanguage = normalizeLanguage(language)
const handleCopy = async () => {
await navigator.clipboard.writeText(code)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className={cn('group relative', className)}>
{title && (
<div
className="flex items-center justify-between rounded-t-lg border-2 border-b-0 px-4 py-2.5"
style={{
borderColor: colors.borders.default,
backgroundColor: colors.backgrounds.card
}}
>
<span className="text-sm font-medium" style={{ color: colors.text.secondary }}>
{title}
</span>
<span className="text-xs font-mono" style={{ color: colors.text.disabled }}>
{normalizedLanguage}
</span>
</div>
)}
<div
className={cn(
'relative overflow-x-auto',
title ? 'rounded-b-lg' : 'rounded-lg',
'border-2'
)}
style={{
borderColor: colors.borders.default,
backgroundColor: colors.backgrounds.cardSolid
}}
>
<button
onClick={handleCopy}
className={cn(
'absolute right-3 top-3 z-10',
'rounded-md px-3 py-1.5',
'text-xs font-medium',
'flex items-center gap-1.5',
'opacity-0 transition-all duration-200',
'group-hover:opacity-100',
copied && 'opacity-100',
effects.transitions.all
)}
style={{
backgroundColor: colors.backgrounds.card,
color: copied ? colors.accents.success : colors.text.muted,
borderWidth: '2px',
borderColor: copied ? colors.accents.success : colors.borders.default
}}
onMouseEnter={(e) => {
if (!copied) {
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
e.currentTarget.style.borderColor = colors.borders.hover
e.currentTarget.style.color = colors.text.secondary
}
}}
onMouseLeave={(e) => {
if (!copied) {
e.currentTarget.style.backgroundColor = colors.backgrounds.card
e.currentTarget.style.borderColor = colors.borders.default
e.currentTarget.style.color = colors.text.muted
}
}}
aria-label="Copy code"
>
{copied ? (
<>
<Check className="h-3.5 w-3.5" />
Copied!
</>
) : (
<>
<Copy className="h-3.5 w-3.5" />
Copy
</>
)}
</button>
<SyntaxHighlighter
language={normalizedLanguage}
style={vscDarkPlus}
showLineNumbers={showLineNumbers}
customStyle={{
margin: 0,
padding: '1rem',
fontSize: '0.875rem',
background: 'transparent',
}}
codeTagProps={{
style: {
fontFamily: 'ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace',
},
}}
>
{code}
</SyntaxHighlighter>
</div>
</div>
)
}

View file

@ -0,0 +1,144 @@
'use client'
import { useState, useEffect, useRef } from 'react'
import { cn } from '@/lib/utils'
import { colors, effects } from '@/lib/theme'
import { Search, X } from 'lucide-react'
import type { DocItem } from '@/lib/docs/types'
interface DocsSearchProps {
items: DocItem[]
onSearch: (query: string) => void
className?: string
}
export default function DocsSearch({
items,
onSearch,
className,
}: DocsSearchProps) {
const [query, setQuery] = useState('')
const [isFocused, setIsFocused] = useState(false)
const inputRef = useRef<HTMLInputElement>(null)
// Keyboard shortcut (Cmd/Ctrl + K)
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if ((e.metaKey || e.ctrlKey) && e.key === 'k') {
e.preventDefault()
inputRef.current?.focus()
}
if (e.key === 'Escape') {
inputRef.current?.blur()
setQuery('')
onSearch('')
}
}
document.addEventListener('keydown', handleKeyDown)
return () => document.removeEventListener('keydown', handleKeyDown)
}, [onSearch])
const handleChange = (value: string) => {
setQuery(value)
onSearch(value)
}
const handleClear = () => {
setQuery('')
onSearch('')
inputRef.current?.focus()
}
return (
<div className={cn('relative', className)}>
<div
className={cn(
'relative flex items-center',
'rounded-lg border-2',
effects.transitions.colors
)}
style={{
borderColor: isFocused ? colors.borders.hover : colors.borders.default,
backgroundColor: colors.backgrounds.card
}}
onMouseEnter={(e) => {
if (!isFocused) {
e.currentTarget.style.borderColor = colors.borders.hover
}
}}
onMouseLeave={(e) => {
if (!isFocused) {
e.currentTarget.style.borderColor = colors.borders.default
}
}}
>
<Search
className="absolute left-3 h-5 w-5"
style={{ color: colors.text.disabled }}
/>
<input
ref={inputRef}
type="text"
value={query}
onChange={(e) => handleChange(e.target.value)}
onFocus={() => setIsFocused(true)}
onBlur={() => setIsFocused(false)}
placeholder="Search documentation..."
className={cn(
'w-full bg-transparent px-10 py-3',
'text-sm outline-none'
)}
style={{
color: colors.text.primary,
caretColor: colors.text.secondary
}}
/>
{query ? (
<button
onClick={handleClear}
className={cn(
'absolute right-3 rounded p-1',
effects.transitions.colors
)}
style={{ color: colors.text.disabled }}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
e.currentTarget.style.color = colors.text.secondary
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = colors.text.disabled
}}
aria-label="Clear search"
>
<X className="h-4 w-4" />
</button>
) : (
<kbd
className={cn(
'absolute right-3',
'rounded border px-2 py-1 text-xs font-mono'
)}
style={{
borderColor: colors.borders.default,
backgroundColor: colors.backgrounds.cardSolid,
color: colors.text.disabled
}}
>
K
</kbd>
)}
</div>
{query && (
<div
className="mt-2 text-xs"
style={{ color: colors.text.disabled }}
>
{items.length} result{items.length !== 1 ? 's' : ''} found
</div>
)}
</div>
)
}

View file

@ -0,0 +1,210 @@
'use client'
import { useState } from 'react'
import { cn } from '@/lib/utils'
import { colors } from '@/lib/theme'
import type { DocNavigation, DocCategory } from '@/lib/docs/types'
import { Settings, Wrench, FileText, Palette, Globe, Package, ChevronDown, ChevronRight, X, Smartphone, Network, BookOpen } from 'lucide-react'
import type { LucideIcon } from 'lucide-react'
interface DocsSidebarProps {
navigation: DocNavigation
currentItemId?: string
className?: string
onClose?: () => void
}
const categoryIcons: Record<DocCategory, LucideIcon> = {
Services: Settings,
Utils: Wrench,
Types: FileText,
Theme: Palette,
Devices: Smartphone,
Domains: Network,
Docs: BookOpen,
API: Globe,
Other: Package,
}
export default function DocsSidebar({
navigation,
currentItemId,
className,
onClose,
}: DocsSidebarProps) {
const [expandedSections, setExpandedSections] = useState<Set<string>>(
new Set(navigation.sections.map((s) => s.title))
)
const isMobileDrawer = !!onClose
const toggleSection = (title: string) => {
const newExpanded = new Set(expandedSections)
if (newExpanded.has(title)) {
newExpanded.delete(title)
} else {
newExpanded.add(title)
}
setExpandedSections(newExpanded)
}
return (
<aside
className={cn(
isMobileDrawer
? 'h-full w-full overflow-y-auto'
: 'sticky top-20 h-[calc(100vh-8rem)] overflow-y-auto w-64',
isMobileDrawer ? 'border-r-0' : 'border-r-2',
className
)}
style={{
borderColor: isMobileDrawer ? 'transparent' : colors.borders.default,
backgroundColor: isMobileDrawer ? colors.backgrounds.cardSolid : 'transparent'
}}
>
{/* Mobile Header with Close Button */}
{isMobileDrawer && (
<div
className="sticky top-0 z-10 flex items-center justify-between p-4 border-b-2"
style={{
backgroundColor: colors.backgrounds.cardSolid,
borderColor: colors.borders.default
}}
>
<h2 className="text-lg font-semibold" style={{ color: colors.text.primary }}>
Navigation
</h2>
<button
onClick={onClose}
className={cn(
'rounded-md p-2',
'transition-colors duration-300'
)}
style={{ color: colors.text.muted }}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
e.currentTarget.style.color = colors.text.secondary
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = colors.text.muted
}}
aria-label="Close navigation"
>
<X className="h-5 w-5" />
</button>
</div>
)}
<nav className="p-4 space-y-2">
{navigation.sections.map((section) => {
const isExpanded = expandedSections.has(section.title)
const Icon = categoryIcons[section.category]
return (
<div key={section.title} className="space-y-1">
<button
onClick={() => toggleSection(section.title)}
className={cn(
'flex w-full items-center gap-2 rounded-md px-3 py-2',
'text-sm font-medium',
'transition-colors duration-300'
)}
style={{
color: colors.text.secondary,
backgroundColor: isExpanded ? colors.backgrounds.hover : 'transparent',
}}
onMouseEnter={(e) => {
if (!isExpanded) {
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
}
}}
onMouseLeave={(e) => {
if (!isExpanded) {
e.currentTarget.style.backgroundColor = 'transparent'
}
}}
>
{isExpanded ? (
<ChevronDown className="h-4 w-4 flex-shrink-0" />
) : (
<ChevronRight className="h-4 w-4 flex-shrink-0" />
)}
<Icon className="h-4 w-4 flex-shrink-0" />
<span className="flex-1">{section.title}</span>
<span
className="text-xs px-1.5 py-0.5 rounded"
style={{
color: colors.text.disabled,
backgroundColor: colors.backgrounds.card
}}
>
{section.items.length}
</span>
</button>
{isExpanded && (
<div className="ml-6 space-y-0.5">
{section.items.map((item) => {
const isActive = item.id === currentItemId
return (
<a
key={item.id}
href={`#${item.id}`}
onClick={isMobileDrawer ? onClose : undefined}
className={cn(
'block rounded-md px-3 py-1.5',
'text-sm transition-colors duration-300'
)}
style={{
color: isActive ? colors.text.primary : colors.text.muted,
backgroundColor: isActive ? colors.backgrounds.hover : 'transparent',
fontWeight: isActive ? 500 : 400
}}
onMouseEnter={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = colors.backgrounds.hover
e.currentTarget.style.color = colors.text.secondary
}
}}
onMouseLeave={(e) => {
if (!isActive) {
e.currentTarget.style.backgroundColor = 'transparent'
e.currentTarget.style.color = colors.text.muted
}
}}
>
<div className="flex items-center gap-2">
<span
className={cn(
'text-xs font-mono px-1.5 py-0.5 rounded flex-shrink-0'
)}
style={{
backgroundColor: colors.backgrounds.card,
color: colors.text.disabled
}}
>
{item.kind === 'function' && 'fn'}
{item.kind === 'method' && 'fn'}
{item.kind === 'class' && 'class'}
{item.kind === 'interface' && 'interface'}
{item.kind === 'type' && 'type'}
{item.kind === 'variable' && 'const'}
{item.kind === 'property' && 'prop'}
{item.kind === 'enum' && 'enum'}
</span>
<span className="truncate">{item.name}</span>
</div>
</a>
)
})}
</div>
)}
</div>
)
})}
</nav>
</aside>
)
}

View file

@ -0,0 +1,296 @@
import { cn } from '@/lib/utils'
import { colors, surfaces, effects } from '@/lib/theme'
import type { DocItem } from '@/lib/docs/types'
import CodeBlock from './CodeBlock'
import TypeLink from './TypeLink'
import { ExternalLink, TriangleAlert } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
interface FunctionDocProps {
item: DocItem
className?: string
availableTypeIds?: Set<string>
}
export default function FunctionDoc({ item, className, availableTypeIds }: FunctionDocProps) {
return (
<div id={item.id} className={cn('scroll-mt-20', className)}>
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="space-y-3">
<div className="flex items-center gap-3 flex-wrap">
<h3 className="text-2xl font-bold" style={{ color: colors.text.primary }}>
{item.name}
</h3>
<span
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium'
)}
style={{
backgroundColor: colors.backgrounds.card,
color: colors.text.secondary
}}
>
{item.kind}
</span>
<span
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium'
)}
style={{
backgroundColor: colors.accents.docsBg,
color: colors.accents.docs,
borderWidth: '1px',
borderColor: colors.accents.docsBorder
}}
>
{item.category}
</span>
{item.deprecated && (
<span
className={cn(
'flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium'
)}
style={{
backgroundColor: colors.accents.warningBg,
color: colors.accents.warning
}}
>
<TriangleAlert className="h-3 w-3" />
Deprecated
</span>
)}
</div>
{item.description && (
<p className="leading-relaxed" style={{ color: colors.text.body }}>
{item.description}
</p>
)}
</div>
{item.source && (
<a
href={`https://github.com/ihatenodejs/aidxnCC/blob/main/${item.source.file}#L${item.source.line}`}
target="_blank"
rel="noopener noreferrer"
className={cn(
'flex items-center gap-1.5 rounded-md px-3 py-2',
'text-xs border-2',
effects.transitions.colors,
'flex-shrink-0'
)}
style={{
color: colors.text.muted,
borderColor: colors.borders.default
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = colors.borders.hover
e.currentTarget.style.color = colors.text.secondary
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = colors.borders.default
e.currentTarget.style.color = colors.text.muted
}}
>
<ExternalLink className="h-3.5 w-3.5" />
Source
</a>
)}
</div>
{/* Remarks */}
{item.remarks && (
<div
className={cn(
'rounded-lg border-l-4 pl-4 py-2',
'space-y-2'
)}
style={{
borderColor: colors.accents.ai,
backgroundColor: colors.backgrounds.card
}}
>
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Remarks
</h4>
<div className="text-sm leading-relaxed prose prose-invert prose-sm max-w-none" style={{ color: colors.text.body }}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{item.remarks}
</ReactMarkdown>
</div>
</div>
)}
{/* Signature */}
{item.signature && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Signature
</h4>
<CodeBlock code={item.signature} language="typescript" />
</div>
)}
{/* Parameters */}
{item.parameters && item.parameters.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Parameters
</h4>
<div className="overflow-x-auto rounded-lg border-2" style={{ borderColor: colors.borders.default }}>
<table className="w-full text-sm">
<thead>
<tr className="border-b-2" style={{ borderColor: colors.borders.default, backgroundColor: colors.backgrounds.card }}>
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
Name
</th>
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
Type
</th>
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
Description
</th>
</tr>
</thead>
<tbody>
{item.parameters.map((param, index) => (
<tr
key={index}
className="border-b last:border-0"
style={{ borderColor: colors.borders.subtle }}
>
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
{param.name}
{param.optional && (
<span style={{ color: colors.text.disabled }}>?</span>
)}
</td>
<td className="px-4 py-3">
<TypeLink type={param.type} className="text-sm" availableTypeIds={availableTypeIds} />
</td>
<td className="px-4 py-3" style={{ color: colors.text.body }}>
{param.description || '—'}
{param.defaultValue && (
<div className="mt-1 text-xs" style={{ color: colors.text.disabled }}>
Default: <code>{param.defaultValue}</code>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Returns */}
{item.returns && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Returns
</h4>
<div
className={cn(
'rounded-lg border-2 p-4',
'space-y-2'
)}
style={{
borderColor: colors.borders.default,
backgroundColor: colors.backgrounds.card
}}
>
<TypeLink type={item.returns.type} className="text-sm" availableTypeIds={availableTypeIds} />
{item.returns.description && (
<p className="text-sm" style={{ color: colors.text.body }}>
{item.returns.description}
</p>
)}
</div>
</div>
)}
{/* Throws */}
{item.throws && item.throws.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Throws
</h4>
<div className="space-y-2">
{item.throws.map((throwsDoc, index) => (
<div
key={index}
className={cn(
'rounded-lg border-2 p-4'
)}
style={{
borderColor: colors.accents.warningBg,
backgroundColor: colors.backgrounds.card
}}
>
<p className="text-sm" style={{ color: colors.text.body }}>
{throwsDoc}
</p>
</div>
))}
</div>
</div>
)}
{/* Examples */}
{item.examples && item.examples.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Examples
</h4>
<div className="space-y-4">
{item.examples.map((example, index) => (
<CodeBlock
key={index}
code={example.code}
language={example.language}
showLineNumbers
/>
))}
</div>
</div>
)}
{/* Tags */}
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{item.tags.map((tag) => (
<span
key={tag}
className={cn(surfaces.badge.muted)}
>
{tag}
</span>
))}
</div>
)}
{/* See Also */}
{item.see && item.see.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
See Also
</h4>
<div className="space-y-2">
{item.see.map((ref, index) => (
<div
key={index}
className="text-sm"
style={{ color: colors.text.body }}
>
{ref}
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}

246
components/docs/TypeDoc.tsx Normal file
View file

@ -0,0 +1,246 @@
import { cn } from '@/lib/utils'
import { colors, surfaces, effects } from '@/lib/theme'
import type { DocItem } from '@/lib/docs/types'
import CodeBlock from './CodeBlock'
import TypeLink from './TypeLink'
import { ExternalLink, TriangleAlert } from 'lucide-react'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
interface TypeDocProps {
item: DocItem
className?: string
availableTypeIds?: Set<string>
}
export default function TypeDoc({ item, className, availableTypeIds }: TypeDocProps) {
return (
<div id={item.id} className={cn('scroll-mt-20', className)}>
<div className="space-y-6">
{/* Header */}
<div className="flex items-start justify-between gap-4">
<div className="space-y-3">
<div className="flex items-center gap-3 flex-wrap">
<h3 className="text-2xl font-bold" style={{ color: colors.text.primary }}>
{item.name}
</h3>
<span
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium'
)}
style={{
backgroundColor: colors.backgrounds.card,
color: colors.text.secondary
}}
>
{item.kind}
</span>
<span
className={cn(
'rounded-md px-2.5 py-1 text-xs font-medium'
)}
style={{
backgroundColor: colors.accents.docsBg,
color: colors.accents.docs,
borderWidth: '1px',
borderColor: colors.accents.docsBorder
}}
>
{item.category}
</span>
{item.deprecated && (
<span
className={cn(
'flex items-center gap-1.5 rounded-md px-2.5 py-1 text-xs font-medium'
)}
style={{
backgroundColor: colors.accents.warningBg,
color: colors.accents.warning
}}
>
<TriangleAlert className="h-3 w-3" />
Deprecated
</span>
)}
</div>
{item.description && (
<p className="leading-relaxed" style={{ color: colors.text.body }}>
{item.description}
</p>
)}
</div>
{item.source && (
<a
href={`https://github.com/ihatenodejs/aidxnCC/blob/main/${item.source.file}#L${item.source.line}`}
target="_blank"
rel="noopener noreferrer"
className={cn(
'flex items-center gap-1.5 rounded-md px-3 py-2',
'text-xs border-2',
effects.transitions.colors,
'flex-shrink-0'
)}
style={{
color: colors.text.muted,
borderColor: colors.borders.default
}}
onMouseEnter={(e) => {
e.currentTarget.style.borderColor = colors.borders.hover
e.currentTarget.style.color = colors.text.secondary
}}
onMouseLeave={(e) => {
e.currentTarget.style.borderColor = colors.borders.default
e.currentTarget.style.color = colors.text.muted
}}
>
<ExternalLink className="h-3.5 w-3.5" />
Source
</a>
)}
</div>
{/* Remarks */}
{item.remarks && (
<div
className={cn(
'rounded-lg border-l-4 pl-4 py-2',
'space-y-2'
)}
style={{
borderColor: colors.accents.ai,
backgroundColor: colors.backgrounds.card
}}
>
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Remarks
</h4>
<div className="text-sm leading-relaxed prose prose-invert prose-sm max-w-none" style={{ color: colors.text.body }}>
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{item.remarks}
</ReactMarkdown>
</div>
</div>
)}
{/* Type Definition */}
{item.signature && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Definition
</h4>
<CodeBlock
code={item.kind === 'interface' ? `interface ${item.name} ${item.signature}` : `${item.kind} ${item.name} = ${item.signature}`}
language="typescript"
/>
</div>
)}
{/* Interface Properties */}
{item.kind === 'interface' && item.parameters && item.parameters.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Properties
</h4>
<div className="overflow-x-auto rounded-lg border-2" style={{ borderColor: colors.borders.default }}>
<table className="w-full text-sm">
<thead>
<tr className="border-b-2" style={{ borderColor: colors.borders.default, backgroundColor: colors.backgrounds.card }}>
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
Property
</th>
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
Type
</th>
<th className="px-4 py-3 text-left font-medium" style={{ color: colors.text.muted }}>
Description
</th>
</tr>
</thead>
<tbody>
{item.parameters.map((prop, index) => (
<tr
key={index}
className="border-b last:border-0"
style={{ borderColor: colors.borders.subtle }}
>
<td className="px-4 py-3 font-mono" style={{ color: colors.text.secondary }}>
{prop.name}
{prop.optional && (
<span style={{ color: colors.text.disabled }}>?</span>
)}
</td>
<td className="px-4 py-3">
<TypeLink type={prop.type} className="text-xs" availableTypeIds={availableTypeIds} />
</td>
<td className="px-4 py-3" style={{ color: colors.text.body }}>
{prop.description || '—'}
{prop.defaultValue && (
<div className="mt-1 text-xs" style={{ color: colors.text.disabled }}>
Default: <code>{prop.defaultValue}</code>
</div>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
{/* Examples */}
{item.examples && item.examples.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
Examples
</h4>
<div className="space-y-4">
{item.examples.map((example, index) => (
<CodeBlock
key={index}
code={example.code}
language={example.language}
showLineNumbers
/>
))}
</div>
</div>
)}
{/* Tags */}
{item.tags && item.tags.length > 0 && (
<div className="flex flex-wrap gap-2">
{item.tags.map((tag) => (
<span
key={tag}
className={cn(surfaces.badge.muted)}
>
{tag}
</span>
))}
</div>
)}
{/* See Also */}
{item.see && item.see.length > 0 && (
<div className="space-y-3">
<h4 className="text-sm font-semibold" style={{ color: colors.text.secondary }}>
See Also
</h4>
<div className="space-y-2">
{item.see.map((ref, index) => (
<div
key={index}
className="text-sm"
style={{ color: colors.text.body }}
>
{ref}
</div>
))}
</div>
</div>
)}
</div>
</div>
)
}

View file

@ -0,0 +1,126 @@
'use client'
import { colors, effects } from '@/lib/theme'
import { cn } from '@/lib/utils'
interface TypeLinkProps {
type: string
className?: string
availableTypeIds?: Set<string>
}
/**
* Parses a type string and converts type references into clickable links
* that scroll to the corresponding type definition in the documentation.
*
* Supports:
* - Simple types: Domain, User, etc.
* - Generic types: Array<Domain>, Promise<User>
* - Union types: string | number
* - Complex types: Record<string, Domain>
*/
export default function TypeLink({ type, className, availableTypeIds }: TypeLinkProps) {
const parseTypeString = (typeStr: string): React.ReactNode[] => {
const parts: React.ReactNode[] = []
let currentIndex = 0
const typeNamePattern = /\b([A-Z][a-zA-Z0-9]*)\b/g
const builtInTypes = new Set([
'string', 'number', 'boolean', 'void', 'null', 'undefined', 'any', 'unknown',
'never', 'object', 'symbol', 'bigint', 'Array', 'Promise', 'Record', 'Partial',
'Required', 'Readonly', 'Pick', 'Omit', 'Exclude', 'Extract', 'NonNullable',
'ReturnType', 'InstanceType', 'ThisType', 'Parameters', 'ConstructorParameters',
'Date', 'Error', 'RegExp', 'Map', 'Set', 'WeakMap', 'WeakSet', 'Function',
'ReadonlyArray', 'String', 'Number', 'Boolean', 'Symbol', 'Object'
])
let match: RegExpExecArray | null
while ((match = typeNamePattern.exec(typeStr)) !== null) {
const typeName = match[1]
const matchStart = match.index
const matchEnd = typeNamePattern.lastIndex
if (matchStart > currentIndex) {
parts.push(
<span key={`text-${currentIndex}`}>
{typeStr.substring(currentIndex, matchStart)}
</span>
)
}
if (builtInTypes.has(typeName)) {
parts.push(
<span key={`builtin-${matchStart}`}>
{typeName}
</span>
)
} else {
// Check if this type exists in the documentation
const typeExists = availableTypeIds?.has(typeName) ?? false
if (typeExists) {
parts.push(
<button
key={`link-${matchStart}`}
onClick={(e) => {
e.preventDefault()
const targetId = typeName
const element = document.getElementById(targetId)
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' })
element.classList.add('ring-2', 'ring-blue-400', 'ring-offset-2', 'ring-offset-gray-900')
setTimeout(() => {
element.classList.remove('ring-2', 'ring-blue-400', 'ring-offset-2', 'ring-offset-gray-900')
}, 2000)
}
}}
className={cn(
'hover:underline cursor-pointer',
effects.transitions.colors
)}
style={{
color: colors.accents.link,
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = colors.accents.linkHover
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = colors.accents.link
}}
>
{typeName}
</button>
)
} else {
// Type doesn't exist in docs, render as plain text
parts.push(
<span key={`text-${matchStart}`}>
{typeName}
</span>
)
}
}
currentIndex = matchEnd
}
if (currentIndex < typeStr.length) {
parts.push(
<span key={`text-${currentIndex}`}>
{typeStr.substring(currentIndex)}
</span>
)
}
return parts
}
return (
<span className={cn('font-mono', className)}>
{parseTypeString(type)}
</span>
)
}

View file

@ -0,0 +1,98 @@
'use client'
import {
getExpirationDate,
getDaysUntilExpiration,
getOwnershipDuration,
getOwnershipMonths,
isExpiringSoon,
formatDate,
getNextRenewalDate
} from '@/lib/domains/utils'
import Link from 'next/link'
import {
Calendar,
Clock,
ChevronRight,
RefreshCw
} from 'lucide-react'
import type { DomainCardProps } from '@/lib/types'
import { domainVisualConfig } from '@/lib/domains/config'
export default function DomainCard({ domain }: DomainCardProps) {
const expirationDate = getExpirationDate(domain)
const nextRenewalDate = getNextRenewalDate(domain)
const daysUntilExpiration = getDaysUntilExpiration(domain)
const ownershipYears = getOwnershipDuration(domain)
const ownershipMonths = getOwnershipMonths(domain)
const expiringSoon = isExpiringSoon(domain)
const statusVisual = domainVisualConfig.status[domain.status]
const categoryVisual = domainVisualConfig.category[domain.category]
const StatusIcon = statusVisual.icon
return (
<Link href={`/domains/${domain.domain}`}>
<div className="group relative h-full bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl hover:border-gray-700 transition-all hover:shadow-xl hover:shadow-black/20 cursor-pointer overflow-hidden flex flex-col">
{expiringSoon && (
<div className="absolute top-0 left-0 right-0 h-1 bg-gray-500"></div>
)}
<div className="p-6 flex flex-col flex-1">
<div className="flex justify-between items-start mb-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-1">
<span className={`${statusVisual.color}`}>
<StatusIcon className="w-4 h-4" />
</span>
<h3 className="text-lg font-semibold text-gray-100 group-hover:text-white transition-colors">
{domain.domain}
</h3>
</div>
<p className="text-sm text-gray-500 line-clamp-2 min-h-[2.5rem]">{domain.usage}</p>
</div>
<ChevronRight className="w-5 h-5 text-gray-600 group-hover:text-gray-400 transition-all group-hover:translate-x-1" />
</div>
<div className="flex items-center gap-4 text-xs text-gray-400 mb-3">
<span className={`${categoryVisual.color} font-medium uppercase tracking-wide`}>
{categoryVisual.label}
</span>
<span className="text-gray-600"></span>
<span>{domain.registrar}</span>
{domain.autoRenew && (
<>
<span className="text-gray-600"></span>
<span className="text-slate-500/80">Auto-renew</span>
</>
)}
</div>
<div className="flex flex-col gap-2 pt-3 border-t border-gray-800/50 mt-auto">
<div className="flex items-center justify-between text-xs">
<div className="flex items-center gap-4">
<div className="flex items-center gap-1.5">
<Clock className="w-3.5 h-3.5 text-gray-500" />
<span className="text-gray-400">
{ownershipYears < 1 ? `${ownershipMonths}mo owned` : `${ownershipYears}y owned`}
</span>
</div>
<div className="flex items-center gap-1.5">
<Calendar className="w-3.5 h-3.5 text-gray-500" />
<span className={expiringSoon ? 'text-gray-300 font-medium' : 'text-gray-400'}>
{expiringSoon ? `${daysUntilExpiration}d left` : formatDate(expirationDate)}
</span>
</div>
</div>
</div>
<div className="flex items-center gap-1.5 text-xs">
<RefreshCw className="w-3.5 h-3.5 text-gray-500" />
<span className="text-gray-400">
Next renewal: {formatDate(nextRenewalDate)}
</span>
</div>
</div>
</div>
</div>
</Link>
)
}

View file

@ -0,0 +1,180 @@
import {
getRegistrationDate,
getExpirationDate,
getDaysUntilExpiration,
getOwnershipDuration,
getOwnershipMonths,
formatDate,
isExpiringSoon,
getRenewalProgress,
getOwnershipDays
} from '@/lib/domains/utils'
import { registrars } from '@/lib/domains/data'
import { domainVisualConfig } from '@/lib/domains/config'
import {
Shield,
Tag,
AlertCircle,
ToggleLeft,
ToggleRight,
Activity
} from 'lucide-react'
import type { DomainDetailsProps } from '@/lib/types'
export default function DomainDetails({ domain }: DomainDetailsProps) {
const registrationDate = getRegistrationDate(domain)
const expirationDate = getExpirationDate(domain)
const daysUntilExpiration = getDaysUntilExpiration(domain)
const ownershipYears = getOwnershipDuration(domain)
const ownershipMonths = getOwnershipMonths(domain)
const ownershipDays = getOwnershipDays(domain)
const expiringSoon = isExpiringSoon(domain)
const renewalProgress = getRenewalProgress(domain)
const registrarConfig = registrars[domain.registrar]
const statusVisual = domainVisualConfig.status[domain.status]
const categoryVisual = domainVisualConfig.category[domain.category]
const StatusIcon = statusVisual.icon
const CategoryIcon = categoryVisual.icon
return (
<div className="space-y-4">
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="grid grid-cols-2 gap-4">
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-2">Status</p>
<div className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg ${statusVisual.bg} ${statusVisual.border} border`}>
<span className={statusVisual.color}>
<StatusIcon className="w-5 h-5" />
</span>
<span className={`font-medium ${statusVisual.color}`}>
{statusVisual.label}
</span>
</div>
</div>
<div>
<p className="text-xs text-gray-500 uppercase tracking-wide mb-2">Category</p>
<div className={`inline-flex items-center gap-2 px-3 py-2 rounded-lg ${categoryVisual.bg} ${categoryVisual.border} border`}>
<span className={categoryVisual.color}>
<CategoryIcon className="w-5 h-5" />
</span>
<span className={`font-medium ${categoryVisual.color}`}>
{categoryVisual.label}
</span>
</div>
</div>
</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h3 className="text-sm font-medium text-gray-300">Domain Lifecycle</h3>
<div className="flex items-center gap-3 text-xs text-gray-500">
Owned for {ownershipDays} days ({ownershipYears < 1 ? `${ownershipMonths} months` : `${ownershipYears} years`})
</div>
</div>
<div className="relative mb-4">
<div className="flex justify-between text-xs text-gray-500 mb-2">
<span>Registered</span>
<span>Expires</span>
</div>
<div className="relative h-4 bg-gray-800 rounded-full overflow-hidden">
<div
className="absolute left-0 top-0 h-full bg-slate-500 rounded-full transition-all duration-500"
style={{ width: `${renewalProgress}%` }}
/>
{expiringSoon && (
<div className="absolute right-0 top-0 h-full w-24 bg-gray-600/30" />
)}
</div>
<div className="flex justify-between text-xs mt-2">
<span className="text-gray-400">{formatDate(registrationDate)}</span>
<span className={`font-medium ${
expiringSoon ? 'text-gray-300' : renewalProgress > 75 ? 'text-slate-400' : 'text-gray-400'
}`}>
{formatDate(expirationDate)}
</span>
</div>
</div>
<div className="grid grid-cols-2 gap-2 text-center">
<div className="p-2 bg-gray-800/50 rounded-lg">
<div className="text-lg font-bold text-slate-400">{Math.floor(renewalProgress)}%</div>
<div className="text-xs text-gray-500">Period Used</div>
</div>
<div className={`p-2 rounded-lg ${expiringSoon ? 'bg-gray-800/70' : 'bg-gray-800/50'}`}>
<div className={`text-lg font-bold ${expiringSoon ? 'text-gray-300' : 'text-slate-400'}`}>
{daysUntilExpiration}
</div>
<div className="text-xs text-gray-500">Days Left</div>
</div>
</div>
{expiringSoon && (
<div className="flex items-center gap-2 p-3 mt-3 bg-gray-800/50 border border-gray-700 rounded-lg">
<AlertCircle className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-400">
Domain expires soon
</span>
</div>
)}
</div>
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="grid grid-cols-2 gap-6">
<div>
<div className="flex items-center gap-2 mb-3">
<Shield className="w-4 h-4 text-gray-500" />
<p className="text-xs text-gray-500 uppercase tracking-wide">Registrar</p>
</div>
<div className="flex items-center gap-2">
{registrarConfig && (
<div className={`w-8 h-8 bg-gray-800 rounded-lg flex items-center justify-center ${registrarConfig.color}`}>
<registrarConfig.icon className="w-4 h-4" />
</div>
)}
<span className="text-gray-200 font-medium">{domain.registrar}</span>
</div>
</div>
<div>
<div className="flex items-center gap-2 mb-3">
<Activity className="w-4 h-4 text-gray-500" />
<p className="text-xs text-gray-500 uppercase tracking-wide">Auto-Renewal</p>
</div>
<button className="flex items-center gap-3 px-3 py-2 rounded-lg bg-gray-800 border border-gray-700 cursor-default">
{domain.autoRenew ? (
<>
<ToggleRight className="w-5 h-5 text-slate-400" />
<span className="text-sm text-slate-400 font-medium">Enabled</span>
</>
) : (
<>
<ToggleLeft className="w-5 h-5 text-gray-500" />
<span className="text-sm text-gray-500 font-medium">Disabled</span>
</>
)}
</button>
</div>
</div>
</div>
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="flex items-center gap-2 mb-4">
<Tag className="w-4 h-4 text-gray-500" />
<h3 className="text-sm font-medium text-gray-300">Tags</h3>
</div>
<div className="flex flex-wrap gap-2">
{domain.tags.map(tag => (
<span
key={tag}
className="px-3 py-1.5 bg-gray-800/50 text-gray-300 rounded-full text-sm hover:bg-gray-800 transition-colors border border-gray-700/50"
>
#{tag}
</span>
))}
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,198 @@
'use client'
import { useState } from 'react'
import { Search, Filter, X } from 'lucide-react'
import type {
DomainFiltersProps,
DomainCategory,
DomainStatus,
DomainRegistrarId,
DomainSortOption
} from '@/lib/types'
import { sortOptions } from '@/lib/domains/config'
export default function DomainFilters({
onSearchChange,
onCategoryChange,
onStatusChange,
onRegistrarChange,
onSortChange,
registrars
}: DomainFiltersProps) {
const [search, setSearch] = useState('')
const [selectedCategories, setSelectedCategories] = useState<DomainCategory[]>([])
const [selectedStatuses, setSelectedStatuses] = useState<DomainStatus[]>([])
const [selectedRegistrars, setSelectedRegistrars] = useState<DomainRegistrarId[]>([])
const [sortBy, setSortBy] = useState<DomainSortOption>('name')
const [showFilters, setShowFilters] = useState(false)
const categories: DomainCategory[] = ['personal', 'service', 'project', 'fun', 'legacy']
const statuses: DomainStatus[] = ['active', 'parked', 'reserved']
const handleSearchChange = (value: string) => {
setSearch(value)
onSearchChange(value)
}
const toggleCategory = (category: DomainCategory) => {
const updated = selectedCategories.includes(category)
? selectedCategories.filter(c => c !== category)
: [...selectedCategories, category]
setSelectedCategories(updated)
onCategoryChange(updated)
}
const toggleStatus = (status: DomainStatus) => {
const updated = selectedStatuses.includes(status)
? selectedStatuses.filter(s => s !== status)
: [...selectedStatuses, status]
setSelectedStatuses(updated)
onStatusChange(updated)
}
const toggleRegistrar = (registrar: DomainRegistrarId) => {
const updated = selectedRegistrars.includes(registrar)
? selectedRegistrars.filter(r => r !== registrar)
: [...selectedRegistrars, registrar]
setSelectedRegistrars(updated)
onRegistrarChange(updated)
}
const handleSortChange = (value: DomainSortOption) => {
setSortBy(value)
onSortChange(value)
}
const clearFilters = () => {
setSearch('')
setSelectedCategories([])
setSelectedStatuses([])
setSelectedRegistrars([])
setSortBy('name')
onSearchChange('')
onCategoryChange([])
onStatusChange([])
onRegistrarChange([])
onSortChange('name')
}
const hasActiveFilters = search || selectedCategories.length > 0 || selectedStatuses.length > 0 || selectedRegistrars.length > 0
return (
<div className="mb-8 space-y-4">
<div className="flex gap-4 items-center">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 text-gray-500 w-5 h-5" />
<input
type="text"
value={search}
onChange={(e) => handleSearchChange(e.target.value)}
placeholder="Search domains..."
className="w-full pl-10 pr-4 py-2 bg-gray-900/50 border border-gray-800 rounded-lg text-gray-200 placeholder-gray-500 focus:outline-none focus:border-gray-700"
/>
</div>
<button
onClick={() => setShowFilters(!showFilters)}
className={`flex items-center gap-2 px-4 py-2 rounded-lg border transition-colors ${
showFilters || hasActiveFilters
? 'bg-gray-800 border-gray-700 text-white'
: 'bg-gray-900/50 border-gray-800 text-gray-400 hover:border-gray-700 hover:text-gray-300'
}`}
>
<Filter className="w-5 h-5" />
Filters
{hasActiveFilters && (
<span className="ml-1 px-2 py-0.5 text-xs bg-slate-500/20 text-slate-400 rounded-full">
Active
</span>
)}
</button>
<select
value={sortBy}
onChange={(e) => handleSortChange(e.target.value as DomainSortOption)}
className="px-4 py-2 bg-gray-900/50 border border-gray-800 rounded-lg text-gray-200 focus:outline-none focus:border-gray-700"
>
{sortOptions.map(option => (
<option key={option.value} value={option.value}>
{option.label}
</option>
))}
</select>
</div>
{showFilters && (
<div className="p-4 bg-gray-900/30 border border-gray-800 rounded-lg space-y-4">
<div className="flex justify-between items-center">
<h3 className="text-sm font-medium text-gray-300">Filter Options</h3>
{hasActiveFilters && (
<button
onClick={clearFilters}
className="text-xs text-gray-500 hover:text-gray-400 flex items-center gap-1"
>
<X className="w-3 h-3" />
Clear all
</button>
)}
</div>
<div>
<h4 className="text-xs text-gray-500 mb-2">Category</h4>
<div className="flex flex-wrap gap-2">
{categories.map(category => (
<button
key={category}
onClick={() => toggleCategory(category)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedCategories.includes(category)
? 'bg-slate-500/20 text-slate-400 border-slate-500/40'
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:border-gray-600'
}`}
>
{category}
</button>
))}
</div>
</div>
<div>
<h4 className="text-xs text-gray-500 mb-2">Status</h4>
<div className="flex flex-wrap gap-2">
{statuses.map(status => (
<button
key={status}
onClick={() => toggleStatus(status)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedStatuses.includes(status)
? 'bg-slate-500/20 text-slate-400 border-slate-500/40'
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:border-gray-600'
}`}
>
{status}
</button>
))}
</div>
</div>
<div>
<h4 className="text-xs text-gray-500 mb-2">Registrar</h4>
<div className="flex flex-wrap gap-2">
{registrars.map(registrar => (
<button
key={registrar}
onClick={() => toggleRegistrar(registrar)}
className={`px-3 py-1 text-sm rounded-full border transition-colors ${
selectedRegistrars.includes(registrar)
? 'bg-slate-500/20 text-slate-400 border-slate-500/40'
: 'bg-gray-800/50 text-gray-400 border-gray-700 hover:border-gray-600'
}`}
>
{registrar}
</button>
))}
</div>
</div>
</div>
)}
</div>
)
}

View file

@ -0,0 +1,88 @@
import { getRenewalTimeline, formatDate, getNextRenewalDate } from '@/lib/domains/utils'
import { Calendar, RefreshCw, Star } from 'lucide-react'
import type { DomainTimelineProps } from '@/lib/types'
export default function DomainTimeline({ domain }: DomainTimelineProps) {
const timeline = getRenewalTimeline(domain)
const nextRenewalDate = getNextRenewalDate(domain)
return (
<div className="bg-gray-900/50 backdrop-blur-sm border border-gray-800 rounded-xl p-6">
<div className="relative">
<div className="absolute left-6 top-8 bottom-0 w-0.5 bg-gray-700"></div>
<div className="space-y-8">
{timeline.map((event, index) => {
const isLatest = index === timeline.length - 1
const isRegistration = event.type === 'registration'
return (
<div key={index} className="relative flex items-start gap-4">
<div className={`relative z-10 flex items-center justify-center w-12 h-12 rounded-full ${
isRegistration || isLatest
? 'bg-gray-800 border-2 border-slate-400/50'
: 'bg-gray-800 border-2 border-gray-700'
}`}>
{isRegistration ? (
<Star className="w-6 h-6 text-slate-300" />
) : (
<RefreshCw className={`w-5 h-5 ${isLatest ? 'text-slate-300' : 'text-gray-500'}`} />
)}
</div>
<div className="flex-1 pb-8">
<div className={`rounded-lg p-4 border transition-colors ${
isRegistration || isLatest
? 'bg-gray-800/50 border-gray-700/50 hover:border-gray-600/50'
: 'bg-slate-400/5 border-slate-400/20 hover:border-slate-400/30'
}`}>
<div className="flex items-center justify-between mb-2">
<span className={`text-sm font-medium ${
isRegistration || isLatest ? 'text-slate-300' : 'text-gray-400'
}`}>
{isRegistration ? 'Domain Registered' : 'Domain Renewed'}
</span>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Calendar className="w-3 h-3" />
{formatDate(event.date)}
</div>
</div>
<div className="text-sm text-gray-300">
{isRegistration ? (
<span>Initial registration for {event.years} {event.years === 1 ? 'year' : 'years'}</span>
) : (
<span>Renewed for {event.years} {event.years === 1 ? 'year' : 'years'}</span>
)}
</div>
</div>
</div>
</div>
)
})}
<div className="relative flex items-start gap-4">
<div className="relative z-10 flex items-center justify-center w-12 h-12 rounded-full bg-gray-900 border-2 border-dashed border-gray-700">
<Calendar className="w-5 h-5 text-gray-600" />
</div>
<div className="flex-1">
<div className="bg-gray-900/30 rounded-lg p-4 border border-dashed border-gray-700/50">
<div className="flex items-center justify-between mb-2">
<span className="text-sm font-medium text-gray-500">
Next Renewal
</span>
<div className="flex items-center gap-2 text-xs text-gray-500">
<Calendar className="w-3 h-3" />
{formatDate(nextRenewalDate)}
</div>
</div>
<div className="text-xs text-gray-600">
{domain.autoRenew ? 'Auto-renewal enabled' : 'Manual renewal required'}
</div>
</div>
</div>
</div>
</div>
</div>
</div>
)
}

View file

@ -0,0 +1,18 @@
import React from 'react'
interface DynadotIconProps {
className?: string
}
export default function DynadotIcon({ className }: DynadotIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 850 968"
fill="currentColor"
className={className}
>
<path d="M0,718.435l0,-34.062c6.563,-84.813 48.281,-160.781 117.188,-210.219c12.031,-8.656 31.874,-19.969 59.531,-33.937c17.687,-8.938 37.969,-19.438 60.812,-31.532c0.532,-0.281 0.719,-0.937 0.438,-1.468l-87.531,-168.063c-0.438,-0.844 -0.094,-1.906 0.75,-2.344c186.406,-96.906 318.687,-165.656 396.875,-206.281c96.187,-49.937 216.875,-37.875 301.843,30.781c135.438,109.469 128.219,320.375 -11.468,424.25c-13.094,9.719 -34.344,22.094 -63.688,37.125c-35.875,18.344 -57.781,29.657 -65.719,33.907c-0.656,0.343 -0.906,1.156 -0.562,1.781l87.375,167.812c0.437,0.844 0.094,1.907 -0.75,2.344c-224.313,116.531 -345.219,179.469 -362.719,188.813c-39.594,21.093 -67.937,34.093 -84.969,38.968c-167.312,47.75 -333.031,-64.437 -347.406,-237.875Zm330.094,-357.75c2,-0.812 18.75,-9.593 50.25,-26.312c24.219,-12.875 41.875,-20.781 52.969,-23.719c61.593,-16.375 124.468,2.031 166.937,49.438c9.25,10.312 19.969,27.5 32.156,51.5c14.282,28.187 23.625,46.187 27.938,54c0.437,0.781 1.437,1.062 2.219,0.625c27.125,-14.438 47.781,-25.188 61.937,-32.25c29.031,-14.563 48.969,-26.469 59.75,-35.688c60.219,-51.594 79.594,-136.937 42.781,-207.562c-36,-69.094 -113.843,-105.344 -190.093,-86.969c-13.407,3.219 -33.532,11.844 -60.375,25.906c-94.157,49.313 -190.344,99.469 -288.625,150.5c-0.844,0.469 -1.188,1.531 -0.719,2.375l40.406,77.281c0.469,0.907 1.563,1.282 2.469,0.875Zm220.031,122.032c0,-42.5 -34.469,-76.969 -76.969,-76.969c-42.5,-0 -76.968,34.469 -76.968,76.969c-0,42.5 34.468,76.968 76.968,76.968c42.5,0 76.969,-34.468 76.969,-76.968Zm66.281,122.187c-2,0.813 -18.781,9.563 -50.375,26.25c-24.281,12.844 -42,20.75 -53.093,23.688c-61.719,16.281 -124.657,-2.25 -167.094,-49.813c-9.25,-10.344 -19.969,-27.562 -32.125,-51.625c-14.281,-28.25 -23.563,-46.281 -27.906,-54.125c-0.438,-0.781 -1.438,-1.094 -2.219,-0.656c-27.188,14.406 -47.875,25.156 -62.063,32.219c-29.125,14.531 -49.094,26.406 -59.906,35.625c-60.406,51.562 -79.969,137 -43.219,207.812c35.938,69.25 113.844,105.688 190.25,87.438c13.406,-3.219 33.594,-11.813 60.5,-25.844c94.375,-49.219 190.813,-99.313 289.313,-150.25c0.875,-0.438 1.187,-1.5 0.75,-2.375l-40.344,-77.469c-0.469,-0.906 -1.563,-1.281 -2.469,-0.875Z" />
</svg>
)
}

View file

@ -0,0 +1,12 @@
import React from 'react'
import { SiGoogle } from 'react-icons/si'
interface GoogleIconProps {
className?: string
strokeWidth?: number
size?: number
}
export default function GoogleIcon({ className, size }: GoogleIconProps) {
return <SiGoogle className={className} size={size} />
}

View file

@ -0,0 +1,25 @@
import React from 'react'
interface KowalskiIconProps {
className?: string
strokeWidth?: number
size?: number
}
export default function KowalskiIcon({ className, size = 24 }: KowalskiIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 400 400"
fill="currentColor"
width={size}
height={size}
className={className}
>
<path d="M179.297 50.376 C 164.092 53.168,147.855 61.349,131.479 74.468 C 113.775 88.651,105.218 103.233,92.361 141.126 C 78.387 182.313,68.874 223.118,48.583 328.906 C 42.461 360.825,38.004 394.166,39.214 398.989 L 39.468 400.000 218.765 400.000 L 398.062 400.000 397.809 398.926 C 395.000 386.997,393.091 381.753,389.221 375.330 C 386.867 371.423,384.640 368.274,373.310 352.842 C 365.699 342.475,359.054 331.666,347.665 311.133 C 341.277 299.615,327.304 275.792,319.140 262.500 C 301.796 234.261,299.201 227.435,298.428 208.024 C 297.409 182.454,294.676 167.052,285.498 135.156 C 278.422 110.564,269.344 94.344,254.114 79.080 C 233.735 58.655,201.519 46.295,179.297 50.376 M248.799 106.489 C 261.588 113.493,267.969 126.130,269.712 147.904 C 270.757 160.959,271.922 164.811,277.307 173.024 C 287.186 188.091,288.511 195.505,285.231 217.383 C 282.919 232.807,283.079 236.616,286.314 243.164 C 288.475 247.539,298.449 263.364,300.698 265.988 C 306.079 272.264,307.804 275.534,311.451 286.369 C 313.507 292.477,314.275 295.779,316.038 306.092 C 318.955 323.145,323.794 340.998,328.706 352.832 C 329.053 353.668,328.477 353.717,327.031 352.973 C 317.514 348.079,306.139 347.859,297.011 352.392 L 293.046 354.362 290.008 353.902 C 285.368 353.198,278.329 351.147,269.938 348.053 C 240.075 337.042,227.498 340.095,217.498 360.781 C 216.210 363.445,215.063 365.625,214.949 365.625 C 214.835 365.625,212.348 363.747,209.422 361.451 C 206.496 359.155,197.773 352.358,190.039 346.345 C 168.528 329.625,163.860 325.786,148.421 312.120 C 144.652 308.784,138.722 303.594,135.241 300.586 C 125.704 292.343,125.397 290.429,130.287 269.695 C 134.458 252.013,134.120 248.138,127.335 235.860 C 118.910 220.615,116.802 212.186,118.504 200.543 C 119.671 192.555,120.387 190.606,124.348 184.630 C 130.549 175.276,130.610 174.884,127.001 167.653 C 120.735 155.103,119.360 142.129,123.311 132.841 C 126.621 125.061,135.901 110.371,138.603 108.638 C 149.303 101.772,171.655 109.150,195.910 127.554 C 209.712 138.026,217.301 140.791,222.032 137.070 C 223.212 136.141,226.543 129.441,229.196 122.656 C 235.502 106.533,240.841 102.130,248.799 106.489 M236.340 143.691 C 230.683 149.637,232.688 170.703,238.910 170.703 C 244.798 170.703,247.446 154.748,243.048 145.766 C 241.118 141.824,238.798 141.107,236.340 143.691 M166.625 145.801 C 161.260 151.182,162.200 169.876,167.956 172.260 C 171.312 173.650,174.196 169.334,174.921 161.837 C 176.037 150.290,171.292 141.119,166.625 145.801 M210.742 166.483 C 210.313 166.727,208.139 168.661,205.912 170.780 C 192.832 183.226,177.161 190.913,152.344 197.056 C 142.302 199.542,142.081 199.608,141.269 200.331 C 138.124 203.130,139.040 206.449,145.002 213.857 C 153.978 225.011,161.812 227.818,190.234 230.066 C 204.734 231.213,213.693 232.795,230.469 237.170 C 241.058 239.932,240.906 239.903,242.542 239.463 C 245.231 238.739,245.970 237.767,249.800 229.930 C 251.807 225.822,254.812 220.352,256.478 217.773 C 274.939 189.203,275.362 186.052,260.938 184.575 C 241.342 182.569,228.672 178.037,219.653 169.807 C 215.348 165.879,213.210 165.081,210.742 166.483 M216.988 180.273 C 224.271 189.549,229.777 201.830,235.700 222.012 C 237.035 226.558,237.043 226.636,236.205 226.410 C 217.682 221.409,205.456 219.237,189.844 218.172 C 174.731 217.142,167.844 216.033,162.791 213.815 C 156.379 211.002,149.455 205.078,152.577 205.078 C 154.767 205.078,173.978 199.068,180.753 196.263 C 191.616 191.766,201.332 185.333,208.970 177.578 L 211.886 174.618 213.210 175.883 C 213.938 176.579,215.638 178.555,216.988 180.273 M241.016 188.474 C 245.324 189.716,252.551 191.076,257.715 191.617 C 259.058 191.758,260.156 191.965,260.156 192.077 C 260.156 192.456,257.282 197.041,252.129 204.883 C 246.676 213.181,243.750 217.912,243.750 218.432 C 243.750 219.784,242.931 218.154,242.193 215.332 C 239.604 205.442,234.470 192.782,230.002 185.275 L 229.259 184.026 232.891 185.603 C 234.889 186.471,238.545 187.763,241.016 188.474"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)
}

View file

@ -0,0 +1,19 @@
import React from 'react'
interface NameIconProps {
className?: string
}
export default function NameIcon({ className }: NameIconProps) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 100 100"
fill="currentColor"
className={className}
>
<path d="m9.7 75v-47.1l13.5-3.3-2.1 15.2h1.5c.8-3.5 2.2-6.5 3.9-9 1.8-2.4 4.1-4.3 6.9-5.6 2.9-1.3 6.1-2 9.9-2 4.9 0 9.1 1.1 12.7 3.4 3.5 2.2 6.2 5.4 8.1 9.7 2 4.2 2.9 9.2 2.9 15v23.7h-13.4v-20.2c0-3.9-.6-7.2-1.8-9.9-1.2-2.8-2.9-4.9-5.2-6.3-2.3-1.5-5-2.2-8.2-2.2-4.8 0-8.5 1.6-11.2 4.8s-4 7.7-4 13.6v20.2z"/>
<circle cx="75" cy="68.5" r="5.7"/>
</svg>
)
}

View file

@ -0,0 +1,310 @@
import React from 'react'
import Link from 'next/link'
import Image from 'next/image'
import {
TbCopyrightOff,
TbMail,
TbBrandGithub,
TbBrandX,
} from "react-icons/tb"
import { ChevronRight } from 'lucide-react'
import RandomFooterMsg from "../objects/RandomFooterMsg"
import { cn } from '@/lib/utils'
import { colors, surfaces } from '@/lib/theme'
import { getRecentGitHubRepos } from '@/lib/github'
import {
footerNavigationLinks,
footerSupportLinks,
} from './footer-config'
import type {
FooterMenuRenderContext,
FooterMenuSection,
NavigationIcon,
} from '@/lib/types/navigation'
const FOOTER_MENU_SECTIONS: FooterMenuSection[] = [
{
type: 'links',
title: 'Navigation',
links: footerNavigationLinks,
},
{
type: 'custom',
title: 'Latest Projects',
render: ({ githubRepos, githubUsername }: FooterMenuRenderContext) => (
githubRepos.length > 0
? githubRepos.map((repo) => (
<FooterLink
key={repo.id}
href={repo.url}
icon={TbBrandGithub}
external
>
<span className="truncate" title={repo.name}>
{repo.name}
</span>
</FooterLink>
))
: (
<FooterLink
href={`https://github.com/${githubUsername}`}
icon={TbBrandGithub}
external
>
Projects unavailable visit GitHub
</FooterLink>
)
),
},
{
type: 'links',
title: 'Support Me',
links: footerSupportLinks,
},
]
interface FooterLinkProps {
href: string
children: React.ReactNode
external?: boolean
icon?: NavigationIcon
}
const FooterLink = ({ href, children, external = false, icon: Icon }: FooterLinkProps) => {
const linkProps = external ? { target: "_blank", rel: "noopener noreferrer" } : {}
return (
<Link
href={href}
{...linkProps}
className={cn(
"flex items-center transition-colors duration-300 group",
"hover:text-white"
)}
style={{ color: colors.text.muted }}
>
{Icon && (
<span className="mr-1.5 group-hover:scale-110 transition-transform">
<Icon size={14} />
</span>
)}
{children}
{external && <ChevronRight size={14} className="ml-0.5 opacity-50 group-hover:opacity-100 transition-opacity" />}
</Link>
)
}
interface FooterSectionProps {
title: string
children: React.ReactNode
}
const FooterSection = ({ title, children }: FooterSectionProps) => (
<div className="flex flex-col space-y-4">
<h3
className="font-semibold text-sm uppercase tracking-wider"
style={{ color: colors.text.secondary }}
>{title}</h3>
<div className="flex flex-col space-y-2.5">
{children}
</div>
</div>
)
type Persona = {
role: string
description: string
}
const personaOptions: Persona[] = [
{
role: 'Chief Synergy Evangelist',
description: 'Drives enterprise-wide alignment through scalable cross-functional touchpoints.'
},
{
role: 'Director of Strategic Buzzwords',
description: 'Operationalizes high-impact vocabulary to maximize stakeholder resonance.'
},
{
role: 'Vice President of Change Management',
description: 'Leads transformational roadmaps that empower teams to pivot at scale.'
},
{
role: 'Global KPI Whisperer',
description: 'Ensures metric integrity through proactive dashboard storytelling.'
},
{
role: 'Head of Agile Communications',
description: 'Facilitates sprint cadence narratives for executive-level consumption.'
},
{
role: 'VP of Continuous Optimization',
description: 'Champions always-on iteration loops to unlock compounding efficiency gains.'
},
{
role: 'Principal Narrative Architect',
description: 'Synthesizes cross-team input into unified, board-ready success frameworks.'
},
{
role: 'Lead Alignment Strategist',
description: 'Converts strategic pivots into measurable OKR cascades and culture moments.'
},
{
role: 'Chief Risk Mitigator',
description: 'De-risks enterprise bets through proactive dependency orchestration.'
},
{
role: 'Director of Value Realization',
description: 'Translates initiatives into quantifiable ROI across all stakeholder tiers.'
}
]
const defaultPersona: Persona = personaOptions[0] ?? {
role: 'Developer & Creator',
description: 'Building thoughtful digital experiences and exploring the intersection of technology, music, and creativity. Currently focused on web development and AI integration.'
}
const getPersonaByIndex = (index: number | undefined): Persona => {
if (!personaOptions.length) {
return defaultPersona
}
if (typeof index !== 'number' || Number.isNaN(index)) {
return defaultPersona
}
const safeIndex = ((Math.floor(index) % personaOptions.length) + personaOptions.length) % personaOptions.length
return personaOptions[safeIndex] ?? defaultPersona
}
interface FooterProps {
footerMessageIndex?: number
}
export default async function Footer({ footerMessageIndex }: FooterProps) {
const persona = getPersonaByIndex(footerMessageIndex)
const { username: githubUsername, repos: githubRepos } = await getRecentGitHubRepos()
return (
<footer
className={cn(surfaces.panel.overlay, "mt-auto border-t")}
style={{ color: colors.text.muted }}
>
<div className="container mx-auto px-4 py-12">
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-[1.2fr_repeat(3,minmax(0,1fr))] gap-x-10 gap-y-12 lg:gap-x-16">
<div className="col-span-1 md:col-span-2 lg:col-span-1">
<div className="flex flex-col space-y-4">
<div className="flex items-center space-x-4">
<div
className="relative w-16 h-16 rounded-full overflow-hidden ring-2"
style={{
backgroundColor: colors.borders.default,
borderColor: colors.borders.hover
}}
>
<Image
src="/ihatenodejs.jpg"
alt="Aidan"
width={64}
height={64}
className="object-cover"
/>
</div>
<div>
<h3
className="font-bold text-lg"
style={{ color: colors.text.primary }}
>Aidan</h3>
<p
className="text-sm"
style={{ color: colors.text.muted }}
>{persona.role}</p>
</div>
</div>
<p
className="text-sm leading-relaxed"
style={{ color: colors.text.muted }}
>{persona.description}</p>
<div className="flex items-center space-x-4 pt-2">
<Link
href={`https://github.com/${githubUsername}`}
target="_blank"
rel="noopener noreferrer"
className="hover:text-white transition-colors"
style={{ color: colors.text.muted }}
aria-label="GitHub"
>
<TbBrandGithub size={20} />
</Link>
<Link
href="https://x.com/aidxnn"
target="_blank"
rel="noopener noreferrer"
className="hover:text-white transition-colors"
style={{ color: colors.text.muted }}
aria-label="X (Twitter)"
>
<TbBrandX size={20} />
</Link>
<Link
href="/contact"
className="hover:text-white transition-colors"
style={{ color: colors.text.muted }}
aria-label="Email"
>
<TbMail size={20} />
</Link>
</div>
</div>
</div>
{FOOTER_MENU_SECTIONS.map((section) => (
<FooterSection key={section.title} title={section.title}>
{section.type === 'links'
? section.links.map(({ href, label, icon, external }) => (
<FooterLink key={href} href={href} icon={icon} external={external}>
{label}
</FooterLink>
))
: section.render({ githubUsername, githubRepos })}
</FooterSection>
))}
</div>
</div>
<div
className="border-t"
style={{
borderColor: colors.borders.muted,
backgroundColor: colors.backgrounds.card
}}
>
<div className="container mx-auto px-4 py-4">
<div className="grid grid-cols-1 sm:grid-cols-[1fr_auto_1fr] items-center gap-y-2">
<div
className="flex items-center justify-center sm:justify-start text-sm"
style={{ color: colors.text.disabled }}
>
<TbCopyrightOff className="mr-2" size={16} />
<span>Open Source and Copyright-Free</span>
</div>
<div className="flex items-center justify-center space-x-2 text-sm">
<RandomFooterMsg index={footerMessageIndex} />
</div>
{/* soon ->
<div className="flex items-center justify-center sm:justify-end space-x-4 text-sm">
<span className="flex items-center">
<span className="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
<span style={{ color: colors.text.disabled }}>All Systems Operational</span>
</span>
</div>*/}<div></div>
</div>
</div>
</div>
</footer>
)
}

View file

@ -0,0 +1,495 @@
"use client"
import React, { useState, useRef, useEffect } from 'react'
import Link from 'next/link'
import {
X,
Menu,
ChevronDown,
ChevronRight,
} from 'lucide-react'
import { cn } from '@/lib/utils'
import { colors, surfaces } from '@/lib/theme'
import type {
NavigationIcon,
NavigationMenuItem,
NavigationDropdownConfig,
NavigationDropdownGroup,
} from '@/lib/types/navigation'
import { headerNavigationConfig } from './header-config'
const NAVIGATION_CONFIG: NavigationMenuItem[] = headerNavigationConfig
interface NavItemProps {
href: string
icon: NavigationIcon
children: React.ReactNode
}
const NavItem = ({ href, icon, children }: NavItemProps) => (
<div className="nav-item">
<Link href={href} className={cn("flex items-center", surfaces.button.nav)}>
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
{children}
</Link>
</div>
);
interface DropdownNavItemProps {
id: string
href: string
icon: NavigationIcon
children: React.ReactNode
dropdownContent: React.ReactNode
isMobile?: boolean
isOpen?: boolean
onOpenChange?: (id: string | null, immediate?: boolean) => 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, true);
}
};
if (isMobile && isOpen) {
document.addEventListener('click', handleClickOutside);
return () => document.removeEventListener('click', handleClickOutside);
}
}, [isMobile, isOpen, onOpenChange]);
const handleMouseEnter = () => {
if (!isMobile) {
onOpenChange?.(id, true);
}
};
const handleMouseLeave = (e: React.MouseEvent) => {
if (!isMobile) {
const relatedTarget = e.relatedTarget as Node | null;
if (relatedTarget instanceof Node && dropdownRef.current?.contains(relatedTarget)) {
return;
}
onOpenChange?.(null);
}
};
const handleClick = (e: React.MouseEvent) => {
if (isMobile) {
e.preventDefault();
e.stopPropagation();
onOpenChange?.(isOpen ? null : id, true);
}
};
return (
<div
className="nav-item relative"
ref={dropdownRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<Link
href={href}
onClick={isMobile ? handleClick : undefined}
className={cn("flex items-center justify-between w-full", surfaces.button.nav)}
>
<span className="flex items-center flex-1">
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
<span>{children}</span>
</span>
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
</Link>
{isOpen && (
<>
{!isMobile && <div className="absolute left-0 top-full w-full h-1 z-50" />}
<div className={isMobile ? 'relative w-full mt-2 ml-5 pr-4' : 'absolute left-0 mt-1 z-50 flex'}>
{dropdownContent}
</div>
</>
)}
</div>
);
};
interface NestedDropdownItemProps {
children: React.ReactNode
nestedContent: React.ReactNode
icon: NavigationIcon
isMobile?: boolean
itemKey: string
activeNested: string | null
onNestedChange: (key: string | null, immediate?: boolean) => void
}
const NestedDropdownItem = ({ children, nestedContent, icon: Icon, isMobile = false, itemKey, activeNested, onNestedChange }: NestedDropdownItemProps) => {
const itemRef = useRef<HTMLDivElement>(null);
const isOpen = activeNested === itemKey;
const handleMouseEnter = () => {
if (!isMobile) {
onNestedChange(itemKey, true);
}
};
const handleMouseLeave = (e: React.MouseEvent) => {
if (!isMobile) {
const relatedTarget = e.relatedTarget as Node | null;
if (relatedTarget instanceof Node && itemRef.current?.contains(relatedTarget)) {
return;
}
onNestedChange(null);
}
};
const handleClick = (e: React.MouseEvent) => {
if (isMobile) {
e.preventDefault();
e.stopPropagation();
onNestedChange(isOpen ? null : itemKey, true);
}
};
if (isMobile) {
return (
<div
className="relative"
ref={itemRef}
>
<button
onClick={handleClick}
className={cn("flex items-center justify-between w-full text-left px-4 py-3 text-sm", surfaces.button.dropdownItem)}
>
<span className="flex items-center flex-1">
<Icon 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-5 pr-4 space-y-1">
{nestedContent}
</div>
)}
</div>
);
}
return (
<div
className="relative"
ref={itemRef}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button
onClick={handleClick}
className={cn(
"flex items-center justify-between w-full text-left px-4 py-3 text-sm",
isOpen ? "bg-gray-700/40 text-white" : surfaces.button.dropdownItem
)}
>
<span className="flex items-center flex-1">
<Icon className="mr-3" strokeWidth={2.5} size={18} />
{children}
</span>
<ChevronDown className={`transform transition-transform duration-200 ${isOpen ? '-rotate-90' : ''}`} strokeWidth={2.5} size={18} />
</button>
{isOpen && (
<>
<div className="absolute left-full top-0 w-4 h-full z-50" />
<div
className={cn(
"absolute left-full top-0 ml-1 w-64 z-50",
"animate-in fade-in-0 zoom-in-95 slide-in-from-left-2 duration-200",
surfaces.panel.dropdown
)}
onMouseEnter={() => onNestedChange(itemKey, true)}
onMouseLeave={(e) => {
const relatedTarget = e.relatedTarget as Node | null;
if (relatedTarget instanceof Node && itemRef.current?.contains(relatedTarget)) return;
onNestedChange(null);
}}
>
{nestedContent}
</div>
</>
)}
</div>
);
};
const renderNestedGroups = (groups: NavigationDropdownGroup[], isMobile: boolean) => {
const hasAnyTitle = groups.some(group => group.title);
return (
<div className={hasAnyTitle ? 'py-2' : ''}>
{groups.map((group, index) => (
<div key={group.title || `group-${index}`}>
{group.title && (
<div
className={cn(
"text-[11px] uppercase tracking-wide",
isMobile ? 'px-4 pt-1 pb-2' : 'px-5 pt-2 pb-2'
)}
style={{ color: colors.text.muted }}
>
{group.title}
</div>
)}
{group.links.map((link) => (
<Link
key={link.href}
href={link.href}
className={cn(
"flex items-center text-sm",
isMobile ? 'px-4 py-2.5' : 'px-5 py-3',
surfaces.button.dropdownItem
)}
{...(link.external && { target: '_blank', rel: 'noopener noreferrer' })}
>
{React.createElement(link.icon, { className: 'mr-3', strokeWidth: 2.5, size: 18 })}
{link.label}
</Link>
))}
</div>
))}
</div>
);
}
const renderDropdownContent = (config: NavigationDropdownConfig, isMobile: boolean, activeNested: string | null, onNestedChange: (key: string | null, immediate?: boolean) => void) => (
<div className={cn(isMobile ? 'w-full' : cn('w-64', surfaces.panel.dropdown))}>
{config.items.map((item) => {
if (item.type === 'link') {
return (
<Link
key={item.href}
href={item.href}
className={cn(
"flex items-center px-4 text-sm",
isMobile ? 'py-2.5' : 'py-3',
surfaces.button.dropdownItem
)}
onMouseEnter={() => {
if (!isMobile && activeNested) {
onNestedChange(null, true);
}
}}
>
{React.createElement(item.icon, { className: 'mr-3', strokeWidth: 2.5, size: 18 })}
{item.label}
</Link>
)
}
return (
<NestedDropdownItem
key={`nested-${item.label}`}
itemKey={`nested-${item.label}`}
icon={item.icon}
isMobile={isMobile}
activeNested={activeNested}
onNestedChange={onNestedChange}
nestedContent={renderNestedGroups(item.groups, isMobile)}
>
{item.label}
</NestedDropdownItem>
)
})}
</div>
)
export default function Header() {
const [isOpen, setIsOpen] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
const [activeNested, setActiveNested] = useState<string | null>(null);
const [showDesktopOverlay, setShowDesktopOverlay] = useState(false);
const [overlayVisible, setOverlayVisible] = useState(false);
const overlayCloseTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const overlayOpenFrameRef = useRef<number | null>(null);
const dropdownTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const nestedTimeoutRef = useRef<NodeJS.Timeout | null>(null);
const toggleMenu = () => {
setIsOpen(!isOpen);
if (isOpen) {
setActiveDropdown(null);
setActiveNested(null);
}
};
const handleDropdownChange = (id: string | null, immediate: boolean = false) => {
if (dropdownTimeoutRef.current) {
clearTimeout(dropdownTimeoutRef.current);
dropdownTimeoutRef.current = null;
}
if (nestedTimeoutRef.current) {
clearTimeout(nestedTimeoutRef.current);
nestedTimeoutRef.current = null;
}
if (id !== null || immediate) {
setActiveDropdown(id);
setActiveNested(null);
} else {
dropdownTimeoutRef.current = setTimeout(() => {
setActiveDropdown(null);
setActiveNested(null);
dropdownTimeoutRef.current = null;
}, 300);
}
};
const handleNestedChange = (key: string | null, immediate: boolean = false) => {
if (nestedTimeoutRef.current) {
clearTimeout(nestedTimeoutRef.current);
nestedTimeoutRef.current = null;
}
if (key !== null || immediate) {
setActiveNested(key);
} else {
nestedTimeoutRef.current = setTimeout(() => {
setActiveNested(null);
nestedTimeoutRef.current = null;
}, 300);
}
};
useEffect(() => {
const checkMobile = () => {
setIsMobile(window.innerWidth < 1024);
};
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
if (isMobile) {
if (overlayOpenFrameRef.current !== null) {
cancelAnimationFrame(overlayOpenFrameRef.current);
overlayOpenFrameRef.current = null;
}
if (overlayCloseTimeoutRef.current !== null) {
clearTimeout(overlayCloseTimeoutRef.current);
overlayCloseTimeoutRef.current = null;
}
setOverlayVisible(false);
setShowDesktopOverlay(false);
} else if (activeDropdown) {
if (overlayCloseTimeoutRef.current !== null) {
clearTimeout(overlayCloseTimeoutRef.current);
overlayCloseTimeoutRef.current = null;
}
setShowDesktopOverlay(true);
overlayOpenFrameRef.current = requestAnimationFrame(() => {
setOverlayVisible(true);
overlayOpenFrameRef.current = null;
});
} else {
if (overlayOpenFrameRef.current !== null) {
cancelAnimationFrame(overlayOpenFrameRef.current);
overlayOpenFrameRef.current = null;
}
setOverlayVisible(false);
overlayCloseTimeoutRef.current = setTimeout(() => {
setShowDesktopOverlay(false);
overlayCloseTimeoutRef.current = null;
}, 300);
}
return () => {
if (overlayOpenFrameRef.current !== null) {
cancelAnimationFrame(overlayOpenFrameRef.current);
overlayOpenFrameRef.current = null;
}
if (overlayCloseTimeoutRef.current !== null) {
clearTimeout(overlayCloseTimeoutRef.current);
overlayCloseTimeoutRef.current = null;
}
};
}, [activeDropdown, isMobile]);
return (
<>
{showDesktopOverlay && (
<div
className={cn(
'fixed inset-0 z-30 pointer-events-none transition-all duration-300 opacity-0 backdrop-blur-none',
overlayVisible && 'opacity-100 backdrop-blur-sm'
)}
aria-hidden="true"
/>
)}
<header className={cn(surfaces.panel.overlay, "sticky top-0 z-50 border-b")}>
{isOpen && (
<div
className="fixed inset-0 backdrop-blur-md z-40 lg:hidden"
onClick={toggleMenu}
/>
)}
<nav className="container mx-auto px-4 py-4 flex justify-between items-center relative z-50">
<Link
href="/"
className={cn(
"text-2xl font-bold transition-all duration-300 hover:glow",
"hover:text-white"
)}
style={{ color: colors.text.body }}
>
aidan.so
</Link>
<button
onClick={toggleMenu}
className="lg:hidden focus:outline-hidden"
style={{ color: colors.text.body }}
>
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
</button>
<ul className={cn(
"flex flex-col lg:flex-row space-y-3 lg:space-y-0 lg:space-x-4",
"absolute lg:static w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto",
"px-2 py-4 lg:p-0 transition-all duration-300 ease-in-out z-50",
"lg:bg-transparent",
isOpen ? 'flex' : 'hidden lg:flex'
)}
style={{ backgroundColor: isMobile ? colors.backgrounds.cardSolid : undefined }}
>
{NAVIGATION_CONFIG.map((item) => {
if (item.type === 'link') {
return (
<NavItem key={item.id} href={item.href} icon={item.icon}>
{item.label}
</NavItem>
)
}
return (
<DropdownNavItem
key={item.id}
id={item.id}
href={item.href}
icon={item.icon}
dropdownContent={renderDropdownContent(item.dropdown, isMobile, activeNested, handleNestedChange)}
isMobile={isMobile}
isOpen={activeDropdown === item.id}
onOpenChange={handleDropdownChange}
>
{item.label}
</DropdownNavItem>
)
})}
</ul>
</nav>
</header>
</>
);
}

View file

@ -0,0 +1,47 @@
import {
House,
User,
Phone,
BookOpen,
CreditCard,
} from 'lucide-react'
import type { NavigationLink } from '@/lib/types/navigation'
import { SiGithubsponsors } from 'react-icons/si'
export const footerNavigationLinks: NavigationLink[] = [
{
href: '/',
label: 'Home',
icon: House
},
{
href: '/about',
label: 'About Me',
icon: User
},
{
href: '/contact',
label: 'Contact',
icon: Phone
},
{
href: '/manifesto',
label: 'Manifesto',
icon: BookOpen
},
]
export const footerSupportLinks: NavigationLink[] = [
{
href: 'https://donate.stripe.com/6oEeWVcXs9L9ctW4gj',
label: 'Donate via Stripe',
icon: CreditCard,
external: true,
},
{
href: 'https://github.com/sponsors/ihatenodejs',
label: 'GitHub Sponsors',
icon: SiGithubsponsors,
external: true,
},
]

View file

@ -0,0 +1,165 @@
import {
House,
Link as LinkIcon,
User,
Phone,
BookOpen,
Brain,
Smartphone,
Headphones,
Briefcase,
Package,
Cloud,
FileText,
} from 'lucide-react'
import { TbUserHeart } from 'react-icons/tb'
import KowalskiIcon from '@/components/icons/KowalskiIcon'
import GoogleIcon from '@/components/icons/GoogleIcon'
import type { NavigationMenuItem } from '@/lib/types/navigation'
export const headerNavigationConfig: NavigationMenuItem[] = [
{
type: 'link',
id: 'home',
label: 'Home',
href: '/',
icon: House,
},
{
type: 'dropdown',
id: 'about',
label: 'About Me',
href: '/about',
icon: User,
dropdown: {
items: [
{
type: 'link',
label: 'Get to Know Me',
href: '/about',
icon: TbUserHeart,
},
{
type: 'nested',
label: 'Devices',
icon: Smartphone,
groups: [
{
title: 'Phones',
links: [
{
type: 'link',
label: 'Pixel 3a XL (bonito)',
href: '/device/bonito',
icon: GoogleIcon,
},
{
type: 'link',
label: 'Pixel 7 Pro (cheetah)',
href: '/device/cheetah',
icon: GoogleIcon,
},
{
type: 'link',
label: 'Pixel 9 Pro (komodo)',
href: '/device/komodo',
icon: GoogleIcon,
},
],
},
{
title: 'DAPs',
links: [
{
type: 'link',
label: 'JM21',
href: '/device/jm21',
icon: Headphones,
},
],
},
],
},
{
type: 'nested',
label: 'Projects',
icon: Briefcase,
groups: [
{
title: '',
links: [
{
type: 'link',
label: 'modules',
href: 'https://modules.lol/',
icon: Package,
external: true,
},
{
type: 'link',
label: 'Kowalski',
href: 'https://kowalski.social/',
icon: KowalskiIcon,
external: true,
},
{
type: 'link',
label: 'p0ntus',
href: 'https://p0ntus.com/',
icon: Cloud,
external: true,
},
],
},
],
},
],
},
},
{
type: 'dropdown',
id: 'ai',
label: 'AI',
href: '/ai',
icon: Brain,
dropdown: {
items: [
{
type: 'link',
label: 'AI Usage',
href: '/ai/usage',
icon: Brain,
},
],
},
},
{
type: 'link',
id: 'contact',
label: 'Contact',
href: '/contact',
icon: Phone,
},
{
type: 'link',
id: 'domains',
label: 'Domains',
href: '/domains',
icon: LinkIcon,
},
{
type: 'link',
id: 'manifesto',
label: 'Manifesto',
href: '/manifesto',
icon: BookOpen,
},
{
type: 'link',
id: 'docs',
label: 'Docs',
href: '/docs',
icon: FileText,
},
]

View file

@ -0,0 +1,4 @@
export { default as Header } from './Header'
export { default as Footer } from './Footer'
export { headerNavigationConfig } from './header-config'
export { footerNavigationLinks, footerSupportLinks } from './footer-config'

View file

@ -4,7 +4,7 @@ import { useEffect } from "react";
export default function AnimatedTitle() {
useEffect(() => {
const title = 'aidxn.cc';
const title = 'aidan.so';
let index = 1;
let forward = true;
const interval = setInterval(() => {

View file

@ -1,22 +1,42 @@
import { default as NextLink } from 'next/link'
import { cn } from '@/lib/theme'
import { externalLinkProps } from '@/lib/utils/styles'
interface LinkProps {
href: string
className?: string
target?: string
rel?: string
variant?: 'default' | 'nav' | 'muted'
external?: boolean
children: React.ReactNode
}
export default function Link(props: LinkProps) {
export default function Link({
href,
className,
target,
rel,
variant = 'default',
external,
children
}: LinkProps) {
const isExternal = external || href.startsWith('http')
const variantStyles = {
default: 'text-blue-400 hover:underline',
nav: 'text-gray-300 hover:text-white',
muted: 'text-gray-400 hover:text-gray-300'
}
return (
<NextLink
href={props.href}
className={`text-blue-400 hover:underline ${props.className}`}
target={props.target}
rel={props.rel}
href={href}
className={cn(variantStyles[variant], className)}
target={target || (isExternal ? externalLinkProps.target : undefined)}
rel={rel || (isExternal ? externalLinkProps.rel : undefined)}
>
{props.children}
{children}
</NextLink>
)
}

View file

@ -0,0 +1,26 @@
import { ReactNode } from 'react'
interface PageHeaderProps {
icon: ReactNode
title: string
subtitle?: string
className?: string
}
export default function PageHeader({ icon, title, subtitle, className }: PageHeaderProps) {
return (
<div className={className}>
<div className="flex flex-col gap-4">
<div className="flex justify-center">
{icon}
</div>
<h1 className="text-4xl font-bold mt-2 text-center text-gray-200 glow">
{title}
</h1>
{subtitle && (
<p className="text-gray-400 text-center">{subtitle}</p>
)}
</div>
</div>
)
}

View file

@ -1,83 +1,46 @@
"use client"
import {
SiNextdotjs,
SiLucide,
SiVercel,
SiSimpleicons,
SiFontawesome,
SiShadcnui,
SiTailwindcss
} from "react-icons/si"
import Link from 'next/link'
import { useState, useEffect } from 'react'
import { footerMessages } from './footerMessages'
export const footerMessages = [
[
"Built with Next.js",
"https://nextjs.org",
<SiNextdotjs key="nextjs" className="text-md mr-2" />
],
[
"Icons by Lucide",
"https://lucide.dev/",
<SiLucide key="lucide" className="text-md mr-2" />
],
[
"Icons by Simple Icons",
"https://simpleicons.org/",
<SiSimpleicons key="simpleicons" className="text-md mr-2" />
],
[
"Font by Vercel",
"https://vercel.com/font",
<SiVercel key="vercel" className="text-md mr-2" />
],
[
"Icons by Font Awesome",
"https://fontawesome.com/",
<SiFontawesome key="fontawesome" className="text-md mr-2" />
],
[
"Components by Shadcn",
"https://ui.shadcn.com/",
<SiShadcnui key="shadcn" className="text-md mr-2" />
],
[
"Styled with Tailwind",
"https://tailwindcss.com/",
<SiTailwindcss key="tailwind" className="text-md mr-2" />
]
]
interface RandomFooterMsgProps {
index?: number
}
export default function RandomFooterMsg() {
const [randomIndex, setRandomIndex] = useState(0)
const [isMounted, setIsMounted] = useState(false)
const fallbackMessage = footerMessages[0] ?? null
useEffect(() => {
setIsMounted(true)
setRandomIndex(Math.floor(Math.random() * footerMessages.length))
}, [])
if (!isMounted) {
const [message, url, icon] = footerMessages[0]
return (
<Link href={String(url)} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
<div className="flex items-center justify-center">
{icon}
{message}
</div>
</Link>
)
const getMessageByIndex = (index: number | undefined) => {
if (!footerMessages.length) {
return null
}
const [message, url, icon] = footerMessages[randomIndex]
if (typeof index !== 'number' || Number.isNaN(index)) {
return fallbackMessage
}
const safeIndex = ((Math.floor(index) % footerMessages.length) + footerMessages.length) % footerMessages.length
return footerMessages[safeIndex] ?? fallbackMessage
}
export default function RandomFooterMsg({ index }: RandomFooterMsgProps) {
const message = getMessageByIndex(index)
if (!message) {
return null
}
const { text, url, Icon } = message
return (
<Link href={String(url)} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
<Link
href={url}
target="_blank"
rel="noopener noreferrer"
className="hover:text-white transition-colors mb-2 sm:mb-0"
>
<div className="flex items-center justify-center">
{icon}
{message}
<Icon className="text-md mr-2" />
{text}
</div>
</Link>
)

View file

@ -0,0 +1,54 @@
import type { IconType } from 'react-icons'
import {
SiFontawesome,
SiLucide,
SiNextdotjs,
SiShadcnui,
SiSimpleicons,
SiTailwindcss,
SiVercel
} from 'react-icons/si'
export type FooterMessage = {
text: string
url: string
Icon: IconType
}
export const footerMessages: FooterMessage[] = [
{
text: 'Built with Next.js',
url: 'https://nextjs.org',
Icon: SiNextdotjs
},
{
text: 'Icons by Lucide',
url: 'https://lucide.dev/',
Icon: SiLucide
},
{
text: 'Icons by Simple Icons',
url: 'https://simpleicons.org/',
Icon: SiSimpleicons
},
{
text: 'Font by Vercel',
url: 'https://vercel.com/font',
Icon: SiVercel
},
{
text: 'Icons by Font Awesome',
url: 'https://fontawesome.com/',
Icon: SiFontawesome
},
{
text: 'Components by Shadcn',
url: 'https://ui.shadcn.com/',
Icon: SiShadcnui
},
{
text: 'Styled with Tailwind',
url: 'https://tailwindcss.com/',
Icon: SiTailwindcss
}
]

72
components/ui/Card.tsx Normal file
View file

@ -0,0 +1,72 @@
import { ReactNode } from 'react'
import { cn, surfaces } from '@/lib/theme'
type CardVariant = keyof typeof surfaces.card
type SectionVariant = keyof typeof surfaces.section
interface CardProps {
children: ReactNode
title?: ReactNode
variant?: CardVariant | SectionVariant
className?: string
spanCols?: number
onClick?: () => void
}
/**
* Versatile card component with optional title and column spanning.
*
* Supports both card and section variants from the theme system.
* Can display an optional title (string or ReactNode with icons) and span multiple grid columns.
*
* @example
* ```tsx
* // Simple card
* <Card variant="default">Content</Card>
*
* // Section card with title
* <Card variant="default" title="My Section">Content</Card>
*
* // Card with icon in title
* <Card title={<div className="flex items-center gap-2"><Icon />Title</div>}>
* Content
* </Card>
*
* // Card spanning 2 columns
* <Card spanCols={2}>Wide content</Card>
* ```
*/
export function Card({
children,
title,
variant = 'default',
className,
spanCols,
onClick
}: CardProps) {
let variantClass: string
if (variant in surfaces.card) {
variantClass = surfaces.card[variant as CardVariant]
} else if (variant in surfaces.section) {
variantClass = surfaces.section[variant as SectionVariant]
} else {
variantClass = surfaces.card.default
}
const colSpanClass = spanCols ? `lg:col-span-${spanCols}` : ''
return (
<div
className={cn(variantClass, colSpanClass, className)}
onClick={onClick}
>
{title && (
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
{title}
</h2>
)}
{children}
</div>
)
}

View file

@ -0,0 +1,41 @@
import { ReactNode } from 'react'
import { cn } from '@/lib/utils'
interface CardGridProps {
children: ReactNode
cols?: '2' | '3' | '4'
className?: string
}
/**
* Responsive card grid layout component.
*
* Provides a consistent grid system for card layouts with mobile-first responsive breakpoints.
* Default is 3 columns (1 on mobile, 2 on tablet, 3 on desktop).
*
* @example
* ```tsx
* <CardGrid cols="3">
* <Card>Card 1</Card>
* <Card>Card 2</Card>
* <Card>Card 3</Card>
* </CardGrid>
* ```
*/
export function CardGrid({
children,
cols = '3',
className
}: CardGridProps) {
const gridClasses = {
'2': 'grid grid-cols-1 md:grid-cols-2 gap-4 p-4',
'3': 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4',
'4': 'grid grid-cols-2 md:grid-cols-4 gap-4'
}
return (
<div className={cn(gridClasses[cols], className)}>
{children}
</div>
)
}

View file

@ -0,0 +1,95 @@
'use client'
import { ChevronLeft, ChevronRight } from 'lucide-react'
import { useState, useMemo, type ReactNode } from 'react'
interface PaginatedCardListProps<T> {
items: T[]
renderItem: (item: T, index: number) => ReactNode
itemsPerPage: number
title: string
icon?: ReactNode
subtitle?: string
/** Function to extract unique key from item */
getItemKey?: (item: T, index: number) => string | number
}
export default function PaginatedCardList<T>({
items,
renderItem,
itemsPerPage,
title,
icon,
subtitle,
getItemKey
}: PaginatedCardListProps<T>) {
const [currentPage, setCurrentPage] = useState(1)
const { totalPages, currentItems, startIndex } = useMemo(() => {
const totalPages = Math.ceil(items.length / itemsPerPage)
const startIndex = (currentPage - 1) * itemsPerPage
const endIndex = startIndex + itemsPerPage
const currentItems = items.slice(startIndex, endIndex)
return { totalPages, currentItems, startIndex }
}, [items, itemsPerPage, currentPage])
const goToNextPage = () => {
if (currentPage < totalPages) {
setCurrentPage(currentPage + 1)
}
}
const goToPreviousPage = () => {
if (currentPage > 1) {
setCurrentPage(currentPage - 1)
}
}
return (
<section className="p-4 sm:p-6 lg:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 flex flex-col min-h-[500px] sm:min-h-[600px]">
<div className="flex flex-col sm:flex-row sm:justify-between gap-2 mb-4 sm:mb-6">
<h2 className="text-xl sm:text-2xl font-semibold text-gray-200 flex items-center gap-2">
{icon}
{title}
</h2>
{subtitle && (
<p className="text-muted-foreground italic text-xs sm:text-sm">{subtitle}</p>
)}
</div>
<div className="space-y-3 sm:space-y-4 flex-grow mb-4 sm:mb-6 min-h-[300px] sm:min-h-[400px]">
{currentItems.map((item, index) => {
const globalIndex = startIndex + index
const key = getItemKey ? getItemKey(item, globalIndex) : globalIndex
return <div key={key}>{renderItem(item, globalIndex)}</div>
})}
</div>
{totalPages > 1 && (
<div className="flex items-center justify-between mt-auto pt-4 sm:pt-6 pb-1 sm:pb-2 border-t border-gray-700">
<button
onClick={goToPreviousPage}
disabled={currentPage === 1}
className="flex items-center gap-1 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm text-gray-300 hover:text-gray-100 disabled:text-gray-600 disabled:cursor-not-allowed transition-colors"
>
<ChevronLeft size={14} className="sm:w-4 sm:h-4" />
<span className="hidden sm:inline">Previous</span>
<span className="sm:hidden">Prev</span>
</button>
<span className="text-xs sm:text-sm text-gray-400">
Page {currentPage} of {totalPages}
</span>
<button
onClick={goToNextPage}
disabled={currentPage === totalPages}
className="flex items-center gap-1 px-2 sm:px-3 py-1.5 sm:py-2 text-xs sm:text-sm text-gray-300 hover:text-gray-100 disabled:text-gray-600 disabled:cursor-not-allowed transition-colors"
>
Next
<ChevronRight size={14} className="sm:w-4 sm:h-4" />
</button>
</div>
)}
</section>
)
}

32
components/ui/Section.tsx Normal file
View file

@ -0,0 +1,32 @@
import { ReactNode } from 'react'
import { cn, surfaces } from '@/lib/theme'
interface SectionProps {
children: ReactNode
variant?: keyof typeof surfaces.section
className?: string
id?: string
title?: ReactNode
}
export function Section({
children,
variant = 'default',
className,
id,
title
}: SectionProps) {
return (
<section
id={id}
className={cn(surfaces.section[variant], className)}
>
{title && (
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
{title}
</h2>
)}
{children}
</section>
)
}

20
components/ui/Surface.tsx Normal file
View file

@ -0,0 +1,20 @@
import { ReactNode } from 'react'
import { cn, surfaces } from '@/lib/theme'
interface SurfaceProps {
children: ReactNode
variant?: keyof typeof surfaces.panel
className?: string
}
export function Surface({
children,
variant = 'dropdown',
className
}: SurfaceProps) {
return (
<div className={cn(surfaces.panel[variant], className)}>
{children}
</div>
)
}

View file

@ -1,17 +1,22 @@
import { SiGithub, SiForgejo } from "react-icons/si"
import { TbStar, TbGitBranch } from "react-icons/tb"
import featuredProjects from "@/public/data/featured.json"
import Link from "next/link"
import { cn } from "@/lib/utils"
import type { FeaturedProject } from "@/lib/github"
export default function GitHubFeatured({ className }: { className?: string }) {
interface FeaturedReposProps {
projects: FeaturedProject[]
className?: string
}
export default function FeaturedRepos({ projects, className }: FeaturedReposProps) {
return (
<div className={cn("grid grid-cols-1 md:grid-cols-2 gap-4", className)}>
{featuredProjects.map((project) => (
{projects.map((project) => (
<div key={project.id} className="bg-gray-800 p-6 rounded-lg shadow-md min-h-[200px] flex flex-col">
<div className="flex-1">
<h3 className="flex items-center justify-center text-xl font-bold text-gray-100 mb-3">
{project.github ? <SiGithub className="mr-2" /> : <SiForgejo className="mr-2" />} {project.name}
{project.platform === 'github' ? <SiGithub className="mr-2" /> : <SiForgejo className="mr-2" />} {project.name}
</h3>
<p className="text-gray-300 grow">{project.description}</p>
</div>

View file

@ -0,0 +1,41 @@
"use client"
import Image from 'next/image'
import { useState } from 'react'
interface GitHubStatsImageProps {
username: string
}
export default function GitHubStatsImage({ username }: GitHubStatsImageProps) {
const [imageError, setImageError] = useState(false)
if (imageError) { return null }
return (
<div className="flex flex-col justify-center items-center w-full mt-4 gap-4">
<Image
src={`https://github-readme-stats.vercel.app/api?username=${username}&theme=dark&show_icons=true&hide_border=true&count_private=true`}
alt={`${username}'s Stats`}
width={420}
height={200}
onError={() => setImageError(true)}
loading="eager"
priority
unoptimized
className="max-w-full h-auto"
/>
<Image
src={`https://github-readme-stats.vercel.app/api/top-langs/?username=${username}&theme=dark&show_icons=true&hide_border=true&layout=compact`}
alt={`${username}'s Top Languages`}
width={300}
height={200}
onError={() => setImageError(true)}
loading="eager"
priority
unoptimized
className="max-w-full h-auto"
/>
</div>
)
}

View file

@ -12,6 +12,7 @@ import { Progress } from "@/components/ui/progress"
import Link from "@/components/objects/Link"
import ScrollTxt from "@/components/objects/MusicText"
import { connectSocket, disconnectSocket } from "@/lib/socket"
import { effects } from '@/lib/theme/effects'
interface LastFmResponse {
album?: {
@ -148,7 +149,7 @@ const NowPlaying: React.FC = () => {
href={nowPlaying.mbid ? `https://musicbrainz.org/release/${nowPlaying.mbid}` : `https://listenbrainz.org/user/p0ntus`}
target="_blank"
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="border-b border-gray-700 px-2 py-0 block" style={{background: effects.gradients.musicPlayer}}
>
<div className="text-center leading-none pb-1">
<ScrollTxt text={nowPlaying.artist_name?.toUpperCase() || ''} type="artist" className="-mt-0.5" />
@ -196,7 +197,7 @@ const NowPlaying: React.FC = () => {
{/* Virtual screen */}
<div className="mx-2 mt-2 flex-1 bg-black overflow-hidden flex flex-col">
{screenOn && (
<div className="bg-gradient-to-b from-gray-700 via-gray-800 to-gray-900 border-b border-gray-700" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}>
<div className="border-b border-gray-700" style={{background: effects.gradients.musicPlayer}}>
<div className="relative flex items-center pr-1 py-0.5">
<FaBluetoothB size={14} className="text-gray-400" />
<div className="absolute left-1/2 transform -translate-x-1/2 text-white text-xs font-medium">{formatTime(currentTime)}</div>
@ -212,7 +213,7 @@ const NowPlaying: React.FC = () => {
)}
{/* Player controls and seekbar */}
{screenOn && nowPlaying.track_name && (
<div className={`bg-gradient-to-b from-gray-700 to-gray-900 ${nowPlaying.release_name ? "pb-3" : "pb-[12.5px]"} flex flex-col items-center`} style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}>
<div className={`${nowPlaying.release_name ? "pb-3" : "pb-[12.5px]"} flex flex-col items-center`} style={{background: effects.gradients.musicPlayer}}>
<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">