feat: better navigation, updated cc.json, icons on homepage, better structure, cleanup manifesto page, NowPlaying.tsx fix
This commit is contained in:
parent
c1f0832f4a
commit
f81b145bf7
9 changed files with 773 additions and 252 deletions
12
README.md
12
README.md
|
@ -15,12 +15,6 @@ Docker is the easiest way to deploy aidxnCC. There are two example `docker-compo
|
||||||
|
|
||||||
Just create a `.env` file with the below variables, run `docker compose -d --build`, and you'll be all set.
|
Just create a `.env` file with the below variables, run `docker compose -d --build`, and you'll be all set.
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
Any and all contributions are welcome! Simply create a pull request and I should have a response to you within a day.
|
|
||||||
|
|
||||||
Please use common sense when contributing :)
|
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
| Variable | Description |
|
| Variable | Description |
|
||||||
|
@ -32,3 +26,9 @@ Please use common sense when contributing :)
|
||||||
This project does not use a custom user agent when interacting with the MusicBrainz API. This is because the LastPlayed component is rendered client-side and user agent support is not universal.
|
This project does not use a custom user agent when interacting with the MusicBrainz API. This is because the LastPlayed component is rendered client-side and user agent support is not universal.
|
||||||
|
|
||||||
If bugs were to occur with my code, I believe it would be easier for MusicBrainz to block this way.
|
If bugs were to occur with my code, I believe it would be easier for MusicBrainz to block this way.
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
Any and all contributions are welcome! Simply create a pull request and I should have a response to you within a day.
|
||||||
|
|
||||||
|
Please use common sense when contributing :)
|
||||||
|
|
|
@ -4,6 +4,7 @@ import Header from '@/components/Header'
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import { SiClaude } from 'react-icons/si'
|
import { SiClaude } from 'react-icons/si'
|
||||||
|
import Link from 'next/link'
|
||||||
import {
|
import {
|
||||||
Line,
|
Line,
|
||||||
BarChart,
|
BarChart,
|
||||||
|
@ -83,8 +84,103 @@ export default function AI() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="flex-1 flex items-center justify-center">
|
<main className="w-full relative">
|
||||||
<div className="text-gray-300">Loading Claude metrics...</div>
|
<Link
|
||||||
|
href="/ai"
|
||||||
|
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
← Back to AI
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
<div className="my-12 text-center">
|
||||||
|
<div className="flex justify-center mb-6">
|
||||||
|
<SiClaude size={60} />
|
||||||
|
</div>
|
||||||
|
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">Claude Code Usage</h1>
|
||||||
|
<p className="text-gray-400">How much I use Claude Code!</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 px-4">
|
||||||
|
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
|
||||||
|
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
|
||||||
|
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
|
||||||
|
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
<div className="p-6 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
|
||||||
|
<div className="h-9 w-32 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
|
||||||
|
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Usage Trend</h2>
|
||||||
|
<div className="flex gap-2 mb-4">
|
||||||
|
<button className="px-3 py-1 rounded bg-gray-700 text-gray-300" disabled>
|
||||||
|
Cost
|
||||||
|
</button>
|
||||||
|
<button className="px-3 py-1 rounded bg-gray-700 text-gray-300" disabled>
|
||||||
|
Tokens
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||||
|
</section>
|
||||||
|
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Model Usage Distribution</h2>
|
||||||
|
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||||
|
</section>
|
||||||
|
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Token Type Breakdown</h2>
|
||||||
|
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||||
|
</section>
|
||||||
|
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Token Composition</h2>
|
||||||
|
<div className="h-[300px] bg-gray-800 rounded animate-pulse" />
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="px-4 pb-4">
|
||||||
|
<section className="p-8 border-2 border-gray-700 rounded-lg">
|
||||||
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Recent Sessions</h2>
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<table className="w-full text-left">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b border-gray-700">
|
||||||
|
<th className="py-2 px-4 text-gray-400">Date</th>
|
||||||
|
<th className="py-2 px-4 text-gray-400">Models Used</th>
|
||||||
|
<th className="py-2 px-4 text-gray-400">Total Tokens</th>
|
||||||
|
<th className="py-2 px-4 text-gray-400">Cost</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{[...Array(5)].map((_, index) => (
|
||||||
|
<tr key={index} className="border-b border-gray-800">
|
||||||
|
<td className="py-2 px-4">
|
||||||
|
<div className="h-5 w-24 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-4">
|
||||||
|
<div className="h-5 w-96 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-4">
|
||||||
|
<div className="h-5 w-16 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</td>
|
||||||
|
<td className="py-2 px-4">
|
||||||
|
<div className="h-5 w-20 bg-gray-800 rounded animate-pulse" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</main>
|
</main>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
@ -114,6 +210,7 @@ export default function AI() {
|
||||||
})
|
})
|
||||||
return acc
|
return acc
|
||||||
}, [] as { name: string; value: number }[])
|
}, [] as { name: string; value: number }[])
|
||||||
|
.sort((a, b) => b.value - a.value)
|
||||||
|
|
||||||
const tokenTypeData = [
|
const tokenTypeData = [
|
||||||
{ name: 'Input', value: data.totals.inputTokens },
|
{ name: 'Input', value: data.totals.inputTokens },
|
||||||
|
@ -123,7 +220,7 @@ export default function AI() {
|
||||||
]
|
]
|
||||||
|
|
||||||
const dailyTrendData = data.daily.map(day => ({
|
const dailyTrendData = data.daily.map(day => ({
|
||||||
date: new Date(day.date).toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
date: new Date(day.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||||
cost: day.totalCost,
|
cost: day.totalCost,
|
||||||
tokens: day.totalTokens / 1000000,
|
tokens: day.totalTokens / 1000000,
|
||||||
inputTokens: day.inputTokens / 1000,
|
inputTokens: day.inputTokens / 1000,
|
||||||
|
@ -137,7 +234,13 @@ export default function AI() {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen flex flex-col">
|
<div className="min-h-screen flex flex-col">
|
||||||
<Header />
|
<Header />
|
||||||
<main className="w-full">
|
<main className="w-full relative">
|
||||||
|
<Link
|
||||||
|
href="/ai"
|
||||||
|
className="absolute top-4 left-4 text-gray-400 hover:text-gray-200 hover:underline transition-colors duration-200 z-10 px-2 py-1 text-sm sm:text-base"
|
||||||
|
>
|
||||||
|
← Back to AI
|
||||||
|
</Link>
|
||||||
<div className="my-12 text-center">
|
<div className="my-12 text-center">
|
||||||
<div className="flex justify-center mb-6">
|
<div className="flex justify-center mb-6">
|
||||||
<SiClaude size={60} />
|
<SiClaude size={60} />
|
||||||
|
@ -227,6 +330,8 @@ export default function AI() {
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }}
|
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }}
|
||||||
formatter={(value: number) => formatCurrency(value)}
|
formatter={(value: number) => formatCurrency(value)}
|
||||||
|
labelStyle={{ color: '#fff' }}
|
||||||
|
itemStyle={{ color: '#fff' }}
|
||||||
/>
|
/>
|
||||||
</PieChart>
|
</PieChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
|
@ -273,10 +378,13 @@ export default function AI() {
|
||||||
<XAxis dataKey="name" stroke="#9ca3af" />
|
<XAxis dataKey="name" stroke="#9ca3af" />
|
||||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} />
|
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} />
|
||||||
<Tooltip
|
<Tooltip
|
||||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
contentStyle={{ backgroundColor: 'rgba(31, 41, 55)', border: '1px solid #374151' }}
|
||||||
formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`}
|
formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`}
|
||||||
/>
|
/>
|
||||||
<Bar dataKey="value" fill="#b1ada1" />
|
<Bar
|
||||||
|
dataKey="value"
|
||||||
|
fill="#b1ada1"
|
||||||
|
/>
|
||||||
</BarChart>
|
</BarChart>
|
||||||
</ResponsiveContainer>
|
</ResponsiveContainer>
|
||||||
</section>
|
</section>
|
||||||
|
@ -317,7 +425,7 @@ export default function AI() {
|
||||||
<tbody>
|
<tbody>
|
||||||
{data.daily.slice(-5).reverse().map((day, index) => (
|
{data.daily.slice(-5).reverse().map((day, index) => (
|
||||||
<tr key={index} className="border-b border-gray-800 hover:bg-gray-800/50">
|
<tr key={index} className="border-b border-gray-800 hover:bg-gray-800/50">
|
||||||
<td className="py-2 px-4 text-gray-300">{new Date(day.date).toLocaleDateString()}</td>
|
<td className="py-2 px-4 text-gray-300">{new Date(day.date + 'T00:00:00').toLocaleDateString()}</td>
|
||||||
<td className="py-2 px-4 text-gray-300">
|
<td className="py-2 px-4 text-gray-300">
|
||||||
{day.modelsUsed.join(', ')}
|
{day.modelsUsed.join(', ')}
|
||||||
</td>
|
</td>
|
||||||
|
|
|
@ -16,7 +16,7 @@ export default function Manifesto() {
|
||||||
Internet Manifesto
|
Internet Manifesto
|
||||||
</h1>
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-6 pt-6">
|
<div className="px-6 pt-12">
|
||||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
|
||||||
1. Empathy and Understanding
|
1. Empathy and Understanding
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -28,6 +28,7 @@ export default function Manifesto() {
|
||||||
<li>Suspend judgment and seek to understand</li>
|
<li>Suspend judgment and seek to understand</li>
|
||||||
<li>Recognize the humanity in every digital interaction</li>
|
<li>Recognize the humanity in every digital interaction</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||||
2. Unconditional Sharing!
|
2. Unconditional Sharing!
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -40,12 +41,14 @@ export default function Manifesto() {
|
||||||
<li>Support open-source principles</li>
|
<li>Support open-source principles</li>
|
||||||
<li>Create extensive documentation on all of my projects</li>
|
<li>Create extensive documentation on all of my projects</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||||
3. Genuine Human Connection
|
3. Genuine Human Connection
|
||||||
</h2>
|
</h2>
|
||||||
<p className="text-gray-300 mb-4">
|
<p className="text-gray-300 mb-4">
|
||||||
I aim to create a genuine human connection with all people I meet, regardless of who or where they are from.
|
I aim to create a genuine human connection with all people I meet, regardless of who or where they are from.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||||
4. Privacy & Self-Hosted Services
|
4. Privacy & Self-Hosted Services
|
||||||
</h2>
|
</h2>
|
||||||
|
@ -62,6 +65,7 @@ export default function Manifesto() {
|
||||||
<li>Focus my services on being free and open</li>
|
<li>Focus my services on being free and open</li>
|
||||||
<li>Suggest and support privacy-focused software</li>
|
<li>Suggest and support privacy-focused software</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||||
I Commit
|
I Commit
|
||||||
</h2>
|
</h2>
|
||||||
|
|
64
app/page.tsx
64
app/page.tsx
|
@ -4,11 +4,26 @@ import Header from '@/components/Header'
|
||||||
import Footer from '@/components/Footer'
|
import Footer from '@/components/Footer'
|
||||||
import Button from '@/components/objects/Button'
|
import Button from '@/components/objects/Button'
|
||||||
import LastPlayed from '@/components/widgets/NowPlaying'
|
import LastPlayed from '@/components/widgets/NowPlaying'
|
||||||
|
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import { CreditCard, Mail, PillBottle, Scale } from 'lucide-react'
|
|
||||||
|
import {CreditCard, Mail, PillBottle, Scale, UserCircle} from 'lucide-react'
|
||||||
|
import { BsArrowClockwise } from "react-icons/bs";
|
||||||
import { FaHandcuffs } from "react-icons/fa6"
|
import { FaHandcuffs } from "react-icons/fa6"
|
||||||
|
import {
|
||||||
|
SiGithubsponsors,
|
||||||
|
SiNextdotjs,
|
||||||
|
SiTailwindcss,
|
||||||
|
SiDocker,
|
||||||
|
SiLinux,
|
||||||
|
SiTypescript,
|
||||||
|
SiClaude,
|
||||||
|
SiPostgresql
|
||||||
|
} from 'react-icons/si'
|
||||||
|
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { SiGithubsponsors } from 'react-icons/si'
|
import {TbHeartHandshake, TbUserHeart, TbMessage} from "react-icons/tb";
|
||||||
|
import {BiDonateHeart} from "react-icons/bi";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
@ -58,7 +73,33 @@ export default function Home() {
|
||||||
|
|
||||||
{mainSections.map((section, secIndex) => (
|
{mainSections.map((section, secIndex) => (
|
||||||
<section key={secIndex} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
<section key={secIndex} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{section}</h2>
|
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{section === t('home.sections.whereYouAre') ? (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<TbHeartHandshake />
|
||||||
|
<span className="align-middle">{section}</span>
|
||||||
|
</div>
|
||||||
|
) : section === t('home.sections.whoIAm') ? (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<UserCircle />
|
||||||
|
<span className="align-middle">{section}</span>
|
||||||
|
</div>
|
||||||
|
) : section === t('home.sections.whatIDo') ? (
|
||||||
|
<div className="flex flex-row items-center gap-2">
|
||||||
|
<TbUserHeart />
|
||||||
|
<span className="align-middle">{section}</span>
|
||||||
|
</div>
|
||||||
|
) : (section)}</h2>
|
||||||
|
{section === t('home.sections.whatIDo') && (
|
||||||
|
<div className="flex flex-row items-center justify-center gap-4 my-8">
|
||||||
|
<SiNextdotjs size={38} />
|
||||||
|
<SiTypescript size={38} />
|
||||||
|
<SiTailwindcss size={38} />
|
||||||
|
<SiPostgresql size={38} />
|
||||||
|
<SiDocker size={38} />
|
||||||
|
<SiLinux size={38} />
|
||||||
|
<SiClaude size={38} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
{mainStrings[secIndex].map((text: string, index: number) => (
|
{mainStrings[secIndex].map((text: string, index: number) => (
|
||||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||||
{text}
|
{text}
|
||||||
|
@ -68,7 +109,10 @@ export default function Home() {
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<section id="contact" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
<section id="contact" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{t('home.contact.title')}</h2>
|
<h2 className="flex flex-row items-center gap-2 text-2xl font-semibold mb-4 text-gray-200">
|
||||||
|
<TbMessage />
|
||||||
|
{t('home.contact.title')}
|
||||||
|
</h2>
|
||||||
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
|
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
|
||||||
<Button
|
<Button
|
||||||
href={'/contact'}
|
href={'/contact'}
|
||||||
|
@ -79,7 +123,10 @@ export default function Home() {
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section id="donation" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
<section id="donation" className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{t('home.donation.title')}</h2>
|
<h2 className="flex flex-row items-center gap-2 text-2xl font-semibold mb-4 text-gray-200">
|
||||||
|
<BiDonateHeart />
|
||||||
|
{t('home.donation.title')}
|
||||||
|
</h2>
|
||||||
<p className="text-gray-300 mb-6">{t('home.donation.description')}</p>
|
<p className="text-gray-300 mb-6">{t('home.donation.description')}</p>
|
||||||
<h4 className="text-lg font-semibold mb-2 text-gray-200">{t('home.donation.charities.title')}</h4>
|
<h4 className="text-lg font-semibold mb-2 text-gray-200">{t('home.donation.charities.title')}</h4>
|
||||||
<div className="grid grid-cols-1 md:grid-cols-2 md:text-sm gap-3">
|
<div className="grid grid-cols-1 md:grid-cols-2 md:text-sm gap-3">
|
||||||
|
@ -104,6 +151,13 @@ export default function Home() {
|
||||||
>
|
>
|
||||||
{t('home.donation.charities.aclu')}
|
{t('home.donation.charities.aclu')}
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button
|
||||||
|
href="https://www.epicrestartfoundation.org"
|
||||||
|
icon={<BsArrowClockwise size={16} />}
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{t('home.donation.charities.epic-restart')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h4 className="text-lg font-semibold mt-5 mb-2 text-gray-200">{t('home.donation.donate.title')}</h4>
|
<h4 className="text-lg font-semibold mt-5 mb-2 text-gray-200">{t('home.donation.donate.title')}</h4>
|
||||||
|
|
|
@ -12,8 +12,11 @@ import {
|
||||||
Menu,
|
Menu,
|
||||||
Globe,
|
Globe,
|
||||||
ChevronDown,
|
ChevronDown,
|
||||||
Brain
|
ChevronRight,
|
||||||
|
Brain,
|
||||||
|
Smartphone
|
||||||
} from 'lucide-react'
|
} from 'lucide-react'
|
||||||
|
import { SiClaude, SiGoogle } from 'react-icons/si'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
interface NavItemProps {
|
interface NavItemProps {
|
||||||
|
@ -31,6 +34,181 @@ const NavItem = ({ href, icon, children }: NavItemProps) => (
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
interface DropdownNavItemProps {
|
||||||
|
id: string;
|
||||||
|
href: string;
|
||||||
|
icon: React.ElementType;
|
||||||
|
children: React.ReactNode;
|
||||||
|
dropdownContent: React.ReactNode;
|
||||||
|
isMobile?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
onOpenChange?: (id: string | null) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DropdownNavItem = ({ id, href, icon, children, dropdownContent, isMobile = false, isOpen = false, onOpenChange }: DropdownNavItemProps) => {
|
||||||
|
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
|
||||||
|
onOpenChange?.(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile && isOpen) {
|
||||||
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}
|
||||||
|
}, [isMobile, isOpen, onOpenChange]);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (!isMobile) {
|
||||||
|
onOpenChange?.(id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: React.MouseEvent) => {
|
||||||
|
if (!isMobile) {
|
||||||
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
|
if (relatedTarget && dropdownRef.current?.contains(relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onOpenChange?.(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
if (isMobile) {
|
||||||
|
e.preventDefault();
|
||||||
|
onOpenChange?.(isOpen ? null : id);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="nav-item relative"
|
||||||
|
ref={dropdownRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={href}
|
||||||
|
onClick={isMobile ? handleClick : undefined}
|
||||||
|
className="flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 w-full"
|
||||||
|
>
|
||||||
|
{React.createElement(icon, { className: "text-md mr-2", strokeWidth: 2.5, size: 20 })}
|
||||||
|
<span className="flex-1">{children}</span>
|
||||||
|
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
||||||
|
</Link>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* Invisible bridge to handle gap */}
|
||||||
|
{!isMobile && (
|
||||||
|
<div className="absolute left-0 top-full w-full h-1 z-50" />
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
className={`${
|
||||||
|
isMobile
|
||||||
|
? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
|
||||||
|
: 'absolute left-0 mt-1 z-50 flex'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{dropdownContent}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface NestedDropdownItemProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
nestedContent: React.ReactNode;
|
||||||
|
isMobile?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const NestedDropdownItem = ({ children, nestedContent, isMobile = false }: NestedDropdownItemProps) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const itemRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (!isMobile) {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: React.MouseEvent) => {
|
||||||
|
if (!isMobile) {
|
||||||
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
|
if (relatedTarget && itemRef.current?.contains(relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = (e: React.MouseEvent) => {
|
||||||
|
if (isMobile) {
|
||||||
|
e.preventDefault();
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
ref={itemRef}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
className="flex items-center justify-between w-full text-left px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300"
|
||||||
|
>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? '-rotate-90' : ''}`} strokeWidth={2.5} size={18} />
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<div className="relative mt-2 ml-4 bg-gray-700/30 rounded-md">
|
||||||
|
{nestedContent}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="relative"
|
||||||
|
ref={itemRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
onClick={handleClick}
|
||||||
|
className="flex items-center justify-between w-full text-left px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300"
|
||||||
|
>
|
||||||
|
<span className="flex items-center">
|
||||||
|
<Smartphone className="mr-3" strokeWidth={2.5} size={18} />
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
<ChevronRight className={`transform transition-transform duration-200 ${isOpen ? 'rotate-0' : 'rotate-90'}`} strokeWidth={2.5} size={18} />
|
||||||
|
</button>
|
||||||
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* Invisible bridge to handle gap */}
|
||||||
|
<div className="absolute left-full top-0 w-2 h-full z-50" />
|
||||||
|
<div className="absolute left-full top-0 ml-2 w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50">
|
||||||
|
{nestedContent}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const LanguageSelector = () => {
|
const LanguageSelector = () => {
|
||||||
const { i18n } = useTranslation();
|
const { i18n } = useTranslation();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
@ -63,9 +241,33 @@ const LanguageSelector = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
}, []);
|
}
|
||||||
|
}, [isMobile]);
|
||||||
|
|
||||||
|
const handleMouseEnter = () => {
|
||||||
|
if (!isMobile) {
|
||||||
|
setIsOpen(true);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMouseLeave = (e: React.MouseEvent) => {
|
||||||
|
if (!isMobile) {
|
||||||
|
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||||
|
if (relatedTarget && dropdownRef.current?.contains(relatedTarget)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsOpen(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClick = () => {
|
||||||
|
if (isMobile) {
|
||||||
|
setIsOpen(!isOpen);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
|
@ -76,33 +278,35 @@ const LanguageSelector = () => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const buttonContent = (
|
|
||||||
<>
|
|
||||||
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
|
|
||||||
{languages.find(lang => lang.code === i18n.language)?.name || 'English'}
|
|
||||||
{!isMobile && (
|
|
||||||
<ChevronDown className={`w-4 h-4 ml-1 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={20} />
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative" ref={dropdownRef}>
|
<div
|
||||||
|
className="relative"
|
||||||
|
ref={dropdownRef}
|
||||||
|
onMouseEnter={handleMouseEnter}
|
||||||
|
onMouseLeave={handleMouseLeave}
|
||||||
|
>
|
||||||
<button
|
<button
|
||||||
onClick={() => setIsOpen(!isOpen)}
|
onClick={handleClick}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
|
className={`flex items-center text-gray-300 hover:text-white hover:bg-gray-700 rounded-md px-3 py-2 transition-all duration-300 ${isMobile ? 'w-full' : ''}`}
|
||||||
aria-expanded={isOpen}
|
aria-expanded={isOpen}
|
||||||
aria-haspopup="true"
|
aria-haspopup="true"
|
||||||
>
|
>
|
||||||
{buttonContent}
|
<Globe className="text-md mr-2" strokeWidth={2.5} size={20} />
|
||||||
|
<span className="flex-1">{languages.find(lang => lang.code === i18n.language)?.name || 'English'}</span>
|
||||||
|
<ChevronDown className={`ml-2 transform transition-transform duration-200 ${isOpen ? 'rotate-180' : ''}`} strokeWidth={2.5} size={16} />
|
||||||
</button>
|
</button>
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
|
<>
|
||||||
|
{/* Invisible bridge to handle gap */}
|
||||||
|
{!isMobile && (
|
||||||
|
<div className="absolute right-0 top-full w-56 h-2 z-50" />
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
className={`${
|
className={`${
|
||||||
isMobile
|
isMobile
|
||||||
? 'relative mt-1 w-full bg-gray-800 rounded-md shadow-lg'
|
? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
|
||||||
: 'absolute right-0 mt-2 w-48 bg-gray-800 rounded-md shadow-lg z-50'
|
: 'absolute right-0 mt-2 w-56 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50'
|
||||||
}`}
|
}`}
|
||||||
role="menu"
|
role="menu"
|
||||||
aria-orientation="vertical"
|
aria-orientation="vertical"
|
||||||
|
@ -112,17 +316,18 @@ const LanguageSelector = () => {
|
||||||
<button
|
<button
|
||||||
key={lang.code}
|
key={lang.code}
|
||||||
onClick={() => changeLanguage(lang.code)}
|
onClick={() => changeLanguage(lang.code)}
|
||||||
className={`block w-full text-left px-4 py-2 text-sm ${
|
className={`block w-full text-left px-5 py-3 text-base rounded-md ${
|
||||||
i18n.language === lang.code
|
i18n.language === lang.code
|
||||||
? 'text-white bg-gray-700'
|
? 'text-white bg-gray-700'
|
||||||
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
||||||
}`}
|
} transition-all duration-300`}
|
||||||
role="menuitem"
|
role="menuitem"
|
||||||
>
|
>
|
||||||
{lang.name}
|
{lang.name}
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -130,10 +335,75 @@ const LanguageSelector = () => {
|
||||||
|
|
||||||
export default function Header() {
|
export default function Header() {
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
|
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||||
|
|
||||||
const toggleMenu = () => setIsOpen(!isOpen);
|
const toggleMenu = () => setIsOpen(!isOpen);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const checkMobile = () => {
|
||||||
|
setIsMobile(window.innerWidth < 1024);
|
||||||
|
};
|
||||||
|
|
||||||
|
checkMobile();
|
||||||
|
window.addEventListener('resize', checkMobile);
|
||||||
|
return () => window.removeEventListener('resize', checkMobile);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const aboutDropdownContent = (
|
||||||
|
<>
|
||||||
|
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
|
||||||
|
<Link href="/about" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||||
|
<User className="mr-3" strokeWidth={2.5} size={18} />
|
||||||
|
About Me
|
||||||
|
</Link>
|
||||||
|
<NestedDropdownItem
|
||||||
|
isMobile={isMobile}
|
||||||
|
nestedContent={
|
||||||
|
<>
|
||||||
|
<Link href="/device/bonito" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||||
|
<SiGoogle className="mr-3" size={18} />
|
||||||
|
Pixel 3a XL (bonito)
|
||||||
|
</Link>
|
||||||
|
<Link href="/device/cheetah" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||||
|
<SiGoogle className="mr-3" size={18} />
|
||||||
|
Pixel 7 Pro (cheetah)
|
||||||
|
</Link>
|
||||||
|
<Link href="/device/komodo" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||||
|
<SiGoogle className="mr-3" size={18} />
|
||||||
|
Pixel 9 Pro (komodo)
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
Devices
|
||||||
|
</NestedDropdownItem>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const aiDropdownContent = (
|
||||||
|
<div className="w-64 bg-gray-800 rounded-lg shadow-xl border border-gray-700">
|
||||||
|
<Link href="/ai" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||||
|
<Brain className="mr-3" strokeWidth={2.5} size={18} />
|
||||||
|
AI
|
||||||
|
</Link>
|
||||||
|
<Link href="/ai/claude" className="flex items-center px-5 py-3 text-base text-gray-300 hover:text-white hover:bg-gray-700 rounded-md transition-all duration-300">
|
||||||
|
<SiClaude className="mr-3" size={18} />
|
||||||
|
Claude Usage
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-30 pointer-events-none transition-all duration-300 ${
|
||||||
|
activeDropdown && !isMobile
|
||||||
|
? 'backdrop-blur-sm opacity-100'
|
||||||
|
: 'backdrop-blur-none opacity-0'
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
<header className="bg-gray-800 relative">
|
<header className="bg-gray-800 relative">
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div
|
<div
|
||||||
|
@ -150,8 +420,28 @@ export default function Header() {
|
||||||
</button>
|
</button>
|
||||||
<ul className={`flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-4 absolute lg:static bg-gray-800 lg:bg-transparent w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto p-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
|
<ul className={`flex flex-col lg:flex-row space-y-2 lg:space-y-0 lg:space-x-4 absolute lg:static bg-gray-800 lg:bg-transparent w-full lg:w-auto left-0 lg:left-auto top-full lg:top-auto p-4 lg:p-0 transition-all duration-300 ease-in-out z-50 ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
|
||||||
<NavItem href="/" icon={House}>Home</NavItem>
|
<NavItem href="/" icon={House}>Home</NavItem>
|
||||||
<NavItem href="/about" icon={User}>About</NavItem>
|
<DropdownNavItem
|
||||||
<NavItem href="/ai" icon={Brain}>AI</NavItem>
|
id="about"
|
||||||
|
href="/about"
|
||||||
|
icon={User}
|
||||||
|
dropdownContent={aboutDropdownContent}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isOpen={activeDropdown === 'about'}
|
||||||
|
onOpenChange={setActiveDropdown}
|
||||||
|
>
|
||||||
|
About
|
||||||
|
</DropdownNavItem>
|
||||||
|
<DropdownNavItem
|
||||||
|
id="ai"
|
||||||
|
href="/ai"
|
||||||
|
icon={Brain}
|
||||||
|
dropdownContent={aiDropdownContent}
|
||||||
|
isMobile={isMobile}
|
||||||
|
isOpen={activeDropdown === 'ai'}
|
||||||
|
onOpenChange={setActiveDropdown}
|
||||||
|
>
|
||||||
|
AI
|
||||||
|
</DropdownNavItem>
|
||||||
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
|
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
|
||||||
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
|
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
|
||||||
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
|
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
|
||||||
|
@ -164,5 +454,6 @@ export default function Header() {
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
</header>
|
</header>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -181,10 +181,10 @@ const NowPlaying: React.FC = () => {
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
className="bg-gradient-to-b from-gray-700 to-gray-900 border-b border-gray-700 px-2 py-0 block" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}
|
className="bg-gradient-to-b from-gray-700 to-gray-900 border-b border-gray-700 px-2 py-0 block" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}
|
||||||
>
|
>
|
||||||
<div className="text-center leading-none">
|
<div className="text-center leading-none pb-1">
|
||||||
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" className="-my-0.5" />
|
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" />
|
||||||
<ScrollTxt text={currentTrack.track_name} type="track" className="-my-0.5" />
|
<ScrollTxt text={currentTrack.track_name} type="track" className="-mt-0.5" />
|
||||||
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" className="-mt-1.5 mb-0.5" />}
|
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" />}
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
{/* Album art */}
|
{/* Album art */}
|
||||||
|
@ -202,70 +202,6 @@ const NowPlaying: React.FC = () => {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{/* Player controls and seekbar */}
|
|
||||||
<div className="bg-gradient-to-b from-gray-700 to-gray-900 pb-2.5 flex flex-col items-center" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}>
|
|
||||||
<div className="flex justify-center items-center gap-0 px-2">
|
|
||||||
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
|
|
||||||
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="skipBackGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stopColor="#f9fafb" />
|
|
||||||
<stop offset="49%" stopColor="#e5e7eb" />
|
|
||||||
<stop offset="51%" stopColor="#6b7280" />
|
|
||||||
<stop offset="100%" stopColor="#d1d5db" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="2" y="4" width="2" height="12" fill="url(#skipBackGradient)" />
|
|
||||||
<polygon points="12,4 6,10 12,16" fill="url(#skipBackGradient)" />
|
|
||||||
<polygon points="20,4 12,10 20,16" fill="url(#skipBackGradient)" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div className="w-[1px] h-6 bg-gray-800 mx-0.5"></div>
|
|
||||||
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
|
|
||||||
<svg width="38" height="38" viewBox="0 0 24 24" className="drop-shadow-sm">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="pauseGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stopColor="#f9fafb" />
|
|
||||||
<stop offset="49%" stopColor="#e5e7eb" />
|
|
||||||
<stop offset="51%" stopColor="#6b7280" />
|
|
||||||
<stop offset="100%" stopColor="#d1d5db" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<rect x="6" y="4" width="4" height="16" fill="url(#pauseGradient)" />
|
|
||||||
<rect x="14" y="4" width="4" height="16" fill="url(#pauseGradient)" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
<div className="w-[1px] h-6 bg-gray-800 mx-1"></div>
|
|
||||||
<button className="hover:drop-shadow-[0_0_8px_rgba(255,255,255,0.9)] hover:filter hover:brightness-110 transition-all duration-200 p-1 rounded-full overflow-hidden">
|
|
||||||
<svg width="38" height="34" viewBox="0 0 24 20" className="drop-shadow-sm">
|
|
||||||
<defs>
|
|
||||||
<linearGradient id="skipForwardGradient" x1="0%" y1="0%" x2="0%" y2="100%">
|
|
||||||
<stop offset="0%" stopColor="#f9fafb" />
|
|
||||||
<stop offset="49%" stopColor="#e5e7eb" />
|
|
||||||
<stop offset="51%" stopColor="#6b7280" />
|
|
||||||
<stop offset="100%" stopColor="#d1d5db" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<polygon points="2,4 9,10 2,16" fill="url(#skipForwardGradient)" />
|
|
||||||
<polygon points="9,4 17,10 9,16" fill="url(#skipForwardGradient)" />
|
|
||||||
<rect x="18" y="4" width="2" height="12" fill="url(#skipForwardGradient)" />
|
|
||||||
</svg>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="relative w-full flex justify-center mt-1">
|
|
||||||
<div className="w-38 h-2 bg-gray-800 rounded-full relative">
|
|
||||||
<div className="absolute inset-0 bg-gradient-to-b from-white to-gray-600 rounded-full" style={{width: `${volume}%`}} />
|
|
||||||
<div
|
|
||||||
className="absolute top-1/2 transform -translate-y-1/2 w-3.5 h-3.5 bg-gradient-to-b from-gray-200 via-gray-300 to-gray-500 rounded-full border border-gray-400 shadow-inner" style={{
|
|
||||||
left: `calc(${volume}% - 8px)`,
|
|
||||||
backgroundImage: 'radial-gradient(circle at 30% 30%, #f0f0f0 0%, #c0c0c0 60%, #808080 100%), repeating-conic-gradient(#f9fafb 0deg 45deg, #9ca3af 45deg 90deg)',
|
|
||||||
backgroundBlendMode: 'overlay',
|
|
||||||
boxShadow: 'inset 0 1px 2px rgba(0,0,0,0.3), 0 1px 2px rgba(255,255,255,0.5)'
|
|
||||||
}}></div>
|
|
||||||
<input type="range" min="0" max="100" value={volume} onChange={(e) => setVolume(Number(e.target.value))} className="absolute top-0 left-0 w-full h-full opacity-0 cursor-pointer" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -19,7 +19,7 @@
|
||||||
"next": "^15.5.2",
|
"next": "^15.5.2",
|
||||||
"react": "^19.1.1",
|
"react": "^19.1.1",
|
||||||
"react-dom": "^19.1.1",
|
"react-dom": "^19.1.1",
|
||||||
"react-i18next": "^15.7.2",
|
"react-i18next": "^15.7.3",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
"recharts": "^3.1.2",
|
"recharts": "^3.1.2",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.3.1",
|
||||||
|
@ -30,8 +30,8 @@
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@tailwindcss/postcss": "^4.1.12",
|
"@tailwindcss/postcss": "^4.1.12",
|
||||||
"@types/node": "^20.19.11",
|
"@types/node": "^20.19.11",
|
||||||
"@types/react": "^19.1.11",
|
"@types/react": "^19.1.12",
|
||||||
"@types/react-dom": "^19.1.8",
|
"@types/react-dom": "^19.1.9",
|
||||||
"eslint": "^9.34.0",
|
"eslint": "^9.34.0",
|
||||||
"eslint-config-next": "15.1.3",
|
"eslint-config-next": "15.1.3",
|
||||||
"postcss": "^8.5.6",
|
"postcss": "^8.5.6",
|
||||||
|
|
|
@ -446,14 +446,142 @@
|
||||||
"cost": 8.052878999999999
|
"cost": 8.052878999999999
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2025-08-23",
|
||||||
|
"inputTokens": 114,
|
||||||
|
"outputTokens": 6030,
|
||||||
|
"cacheCreationTokens": 408902,
|
||||||
|
"cacheReadTokens": 2606990,
|
||||||
|
"totalTokens": 3022036,
|
||||||
|
"totalCost": 11.605633500000005,
|
||||||
|
"modelsUsed": [
|
||||||
|
"claude-opus-4-1-20250805",
|
||||||
|
"claude-sonnet-4-20250514"
|
||||||
|
],
|
||||||
|
"modelBreakdowns": [
|
||||||
|
{
|
||||||
|
"modelName": "claude-opus-4-1-20250805",
|
||||||
|
"inputTokens": 88,
|
||||||
|
"outputTokens": 5424,
|
||||||
|
"cacheCreationTokens": 387862,
|
||||||
|
"cacheReadTokens": 2545780,
|
||||||
|
"cost": 11.499202500000006
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modelName": "claude-sonnet-4-20250514",
|
||||||
|
"inputTokens": 26,
|
||||||
|
"outputTokens": 606,
|
||||||
|
"cacheCreationTokens": 21040,
|
||||||
|
"cacheReadTokens": 61210,
|
||||||
|
"cost": 0.106431
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2025-08-26",
|
||||||
|
"inputTokens": 2836,
|
||||||
|
"outputTokens": 22779,
|
||||||
|
"cacheCreationTokens": 465292,
|
||||||
|
"cacheReadTokens": 11182259,
|
||||||
|
"totalTokens": 11673166,
|
||||||
|
"totalCost": 25.288227900000006,
|
||||||
|
"modelsUsed": [
|
||||||
|
"claude-opus-4-1-20250805",
|
||||||
|
"claude-sonnet-4-20250514"
|
||||||
|
],
|
||||||
|
"modelBreakdowns": [
|
||||||
|
{
|
||||||
|
"modelName": "claude-opus-4-1-20250805",
|
||||||
|
"inputTokens": 2745,
|
||||||
|
"outputTokens": 19221,
|
||||||
|
"cacheCreationTokens": 405641,
|
||||||
|
"cacheReadTokens": 10473081,
|
||||||
|
"cost": 24.798140250000003
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"modelName": "claude-sonnet-4-20250514",
|
||||||
|
"inputTokens": 91,
|
||||||
|
"outputTokens": 3558,
|
||||||
|
"cacheCreationTokens": 59651,
|
||||||
|
"cacheReadTokens": 709178,
|
||||||
|
"cost": 0.49008765
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2025-08-30",
|
||||||
|
"inputTokens": 151,
|
||||||
|
"outputTokens": 63263,
|
||||||
|
"cacheCreationTokens": 430727,
|
||||||
|
"cacheReadTokens": 4992045,
|
||||||
|
"totalTokens": 5486186,
|
||||||
|
"totalCost": 20.311188749999992,
|
||||||
|
"modelsUsed": [
|
||||||
|
"claude-opus-4-1-20250805"
|
||||||
|
],
|
||||||
|
"modelBreakdowns": [
|
||||||
|
{
|
||||||
|
"modelName": "claude-opus-4-1-20250805",
|
||||||
|
"inputTokens": 151,
|
||||||
|
"outputTokens": 63263,
|
||||||
|
"cacheCreationTokens": 430727,
|
||||||
|
"cacheReadTokens": 4992045,
|
||||||
|
"cost": 20.311188749999992
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2025-08-31",
|
||||||
|
"inputTokens": 108,
|
||||||
|
"outputTokens": 777,
|
||||||
|
"cacheCreationTokens": 40539,
|
||||||
|
"cacheReadTokens": 305195,
|
||||||
|
"totalTokens": 346619,
|
||||||
|
"totalCost": 1.2777937499999998,
|
||||||
|
"modelsUsed": [
|
||||||
|
"claude-opus-4-1-20250805"
|
||||||
|
],
|
||||||
|
"modelBreakdowns": [
|
||||||
|
{
|
||||||
|
"modelName": "claude-opus-4-1-20250805",
|
||||||
|
"inputTokens": 108,
|
||||||
|
"outputTokens": 777,
|
||||||
|
"cacheCreationTokens": 40539,
|
||||||
|
"cacheReadTokens": 305195,
|
||||||
|
"cost": 1.2777937499999998
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"date": "2025-09-01",
|
||||||
|
"inputTokens": 592,
|
||||||
|
"outputTokens": 28240,
|
||||||
|
"cacheCreationTokens": 712734,
|
||||||
|
"cacheReadTokens": 12698327,
|
||||||
|
"totalTokens": 13439893,
|
||||||
|
"totalCost": 34.53813299999999,
|
||||||
|
"modelsUsed": [
|
||||||
|
"claude-opus-4-1-20250805"
|
||||||
|
],
|
||||||
|
"modelBreakdowns": [
|
||||||
|
{
|
||||||
|
"modelName": "claude-opus-4-1-20250805",
|
||||||
|
"inputTokens": 592,
|
||||||
|
"outputTokens": 28240,
|
||||||
|
"cacheCreationTokens": 712734,
|
||||||
|
"cacheReadTokens": 12698327,
|
||||||
|
"cost": 34.53813299999999
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"totals": {
|
"totals": {
|
||||||
"inputTokens": 206310,
|
"inputTokens": 210111,
|
||||||
"outputTokens": 1126501,
|
"outputTokens": 1247590,
|
||||||
"cacheCreationTokens": 23364621,
|
"cacheCreationTokens": 25422815,
|
||||||
"cacheReadTokens": 491053910,
|
"cacheReadTokens": 522838726,
|
||||||
"totalCost": 617.5983253500001,
|
"totalCost": 710.6193022500001,
|
||||||
"totalTokens": 515751342
|
"totalTokens": 549719242
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
{
|
{
|
||||||
"home": {
|
"home": {
|
||||||
"whoAmI": [
|
"whoAmI": [
|
||||||
"Hey there! My name is Aidan, and I'm a systems administrator, full-stack developer, and student from the United States. I primarily work with Linux, Docker, Next.js, Tailwind CSS and TypeScript.",
|
"Hey there! My name is Aidan, and I'm a systems administrator, full-stack developer, and student from the Boston area. I primarily work with Linux, Docker, Next.js, Tailwind CSS and TypeScript.",
|
||||||
"My favorite projects and hobbies include web development and SysAdmin. Most of my work is released into the public domain.",
|
"My favorite projects and hobbies revolve around web development and SysAdmin. Most of my work is released into the public domain.",
|
||||||
"I'm also a huge advocate for AI, and it's practical applications to programming and life itself. I am fond of open-source models the most, specifically Qwen3!",
|
"I'm also a huge advocate for AI and it's practical applications to programming and life itself. I am fond of open-source models the most, specifically Qwen3!",
|
||||||
"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and jumping between projects."
|
"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and jumping between projects. I tend to be quite depressed, but I make do."
|
||||||
],
|
],
|
||||||
"whatIDo": [
|
"whatIDo": [
|
||||||
"I'm at my best when I'm doing system administration and development in TypeScript. I frequently implement AI into my workflow.",
|
"I'm at my best when I'm doing system administration and development in TypeScript. I frequently implement AI into my workflow.",
|
||||||
|
@ -13,7 +13,7 @@
|
||||||
],
|
],
|
||||||
"whereYouAre": [
|
"whereYouAre": [
|
||||||
"I am not here to brag about my accomplishments or plug my shitty SaaS. That's why I've made every effort to make this website as personal and fun as possible.",
|
"I am not here to brag about my accomplishments or plug my shitty SaaS. That's why I've made every effort to make this website as personal and fun as possible.",
|
||||||
"I hope you find this website an interesting place to find more about me, but also learn something new, and inspire a new project or two.",
|
"I hope you find this website an interesting place to find more about me, but also learn something new; maybe inspire a new project or two.",
|
||||||
"In a technical sense, this site is hosted on my dedicated server hosted in Buffalo, New York by ColoCrossing."
|
"In a technical sense, this site is hosted on my dedicated server hosted in Buffalo, New York by ColoCrossing."
|
||||||
],
|
],
|
||||||
"sections": {
|
"sections": {
|
||||||
|
@ -31,10 +31,10 @@
|
||||||
"description": "Feeling generous? Support me or one of the causes I support!",
|
"description": "Feeling generous? Support me or one of the causes I support!",
|
||||||
"charities": {
|
"charities": {
|
||||||
"title": "Charities",
|
"title": "Charities",
|
||||||
"description": "I support the following charities:",
|
|
||||||
"unsilenced": "Unsilenced",
|
"unsilenced": "Unsilenced",
|
||||||
"drugpolicy": "Drug Policy Alliance",
|
"drugpolicy": "Drug Policy Alliance",
|
||||||
"aclu": "ACLU"
|
"aclu": "ACLU",
|
||||||
|
"epic-restart": "EPIC Restart Foundation"
|
||||||
},
|
},
|
||||||
"donate": {
|
"donate": {
|
||||||
"title": "Donate to Me",
|
"title": "Donate to Me",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue