refactor/feat: ipod nano 7g-style now playing component, discontinue fontawesome, content updates

- redesign NowPlaying widget with iPod Nano 7th generation-inspired UI
- content updates (home, about)
- general code cleanup
- bump deps, clean up
- update README for self hosting
- remove old workflows
This commit is contained in:
Aidan 2025-08-02 02:39:24 -04:00
parent db86ce3277
commit d613b58dd6
17 changed files with 466 additions and 460 deletions

View file

@ -1,30 +0,0 @@
name: Run ESLint
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
lint:
name: Run ESLint
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '23'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run ESLint
run: npm run lint

View file

@ -1,41 +0,0 @@
# Credits to https://docs.github.com/en/actions/use-cases-and-examples/publishing-packages/publishing-docker-images
name: Push to Docker Hub
on:
push:
branches:
- main
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: p0ntus/aidxncc
- name: Build and push Docker image
id: push
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
with:
context: .
file: ./Dockerfile
push: true
tags: p0ntus/aidxncc:latest
labels: ${{ steps.meta.outputs.labels }}

View file

@ -1,36 +1,19 @@
# aidxnCC
[![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/)
[![Build Status](https://git.pontusmail.org/aidan/aidxnCC/actions/workflows/push.yml/badge.svg)](https://git.pontusmail.org/aidan/aidxnCC/actions/?workflow=push.yml)
[![ESLint Status](https://git.pontusmail.org/aidan/aidxnCC/actions/workflows/lint.yml/badge.svg)](https://git.pontusmail.org/aidan/aidxnCC/actions/?workflow=lint.yml)
aidxnCC is the third version of my personal website.
It's built with Next.js and Tailwind CSS. aidxnCC will always be a work in progress, though completely functional.
## Deploy
## Deploy with Docker
### Vercel
Docker is the easiest way to deploy aidxnCC. There are two example `docker-compose.yml` files for you to use.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fihatenodejs%2FaidxnCC&env=BRAINZ_USER_AGENT,LISTENBRAINZ_TOKEN&envDescription=You%20will%20need%20both%20a%20custom%20user%20agent%20(for%20identifying%20yourself%20to%20MusicBrainz)%2C%20and%20a%20ListenBrainz%20User%20Token.%20See%20the%20README%20for%20more%20information.&envLink=https%3A%2F%2Fgithub.com%2Fihatenodejs%2FaidxnCC&project-name=aidxn-cc&repository-name=aidxnCC)
1. `docker-compose.yml` - Default, exposed on port 3000
2. `docker-compose.nginx.yml` - Helpful for NGINX Proxy Manager usage w/ Docker networks
To deploy with Vercel, simply click the button above. When prompted for environment variables, see the section below.
### Cloudflare
I currently host aidxnCC on Cloudflare Pages. They currently don't have a "Deploy to Cloudflare" button for Pages, but you can setup like so:
1. Fork `aidxnCC` to your own account
2. Deploy to Pages from your fork
> [!NOTE]
> Make sure to set your environment variables (see below!)
>
> You may also have to set the `nodejs_compat` compatibility flag in the Pages settings.
### Self-Host
**Own a server? Deploy on your own!** F*** SaaS, check out [Coolify](https://coolify.io/), a free and open-source alternative to Vercel.
Just create a `.env` file with the below variables, run `docker compose -d --build`, and you'll be all set.
## Contributing

View file

@ -33,9 +33,11 @@ export default function About() {
<div className="min-h-screen flex flex-col">
<Header />
<main className="text-center py-12">
<div className='flex flex-col items-center justify-center gap-6 mb-6'>
<div className="flex flex-col gap-4">
<div className="flex justify-center">
<User size={60} />
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
</div>
<h1 className="text-4xl font-bold mt-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
{t('about.title')}
</h1>
</div>
@ -109,7 +111,7 @@ export default function About() {
<h3 className={cn("text-xl font-semibold mb-2 text-gray-200", key === "Laptops" && "mt-4")}>{key}</h3>
{(value as unknown as string[]).map((text: string, index: number) => (
<p key={index} className="text-gray-300 leading-relaxed mt-2">
{text.split(/(KernelSU-Next|LineageOS 22.2|Android 16|NixOS|Xubuntu)/).map((part, i) => {
{text.split(/(KernelSU-Next|LineageOS 22.2|Android 16|Xubuntu)/).map((part, i) => {
if (part === 'KernelSU-Next') {
return <Link key={i} href="https://github.com/KernelSU-Next/KernelSU-Next">KernelSU-Next</Link>
}
@ -119,8 +121,8 @@ export default function About() {
if (part === 'Android 16') {
return <Link key={i} href="https://developer.android.com/about/versions/16/get">Android 16</Link>
}
if (part === 'NixOS') {
return <Link key={i} href="https://nixos.org/">NixOS</Link>
if (part === 'OpenCore') {
return <Link key={i} href="https://github.com/acidanthera/OpenCorePkg">OpenCore</Link>
}
if (part === 'Xubuntu') {
return <Link key={i} href="https://xubuntu.org/">Xubuntu</Link>
@ -133,14 +135,16 @@ export default function About() {
<div className="flex flex-row justify-center gap-4 mt-4">
<Button
href="/device/cheetah"
label="Pixel 7 Pro"
icon={Smartphone}
/>
icon={<Smartphone />}
>
Pixel 7 Pro
</Button>
<Button
href="/device/bonito"
label="Pixel 3a XL"
icon={Smartphone}
/>
icon={<Smartphone />}
>
Pixel 3a XL
</Button>
</div>
)}
</div>

View file

@ -2,11 +2,11 @@
import Header from '@/components/Header'
import Footer from '@/components/Footer'
import ContactButton from '@/components/objects/ContactButton'
import Button from '@/components/objects/Button'
import { Phone } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { faPhone, faEnvelope } from '@fortawesome/free-solid-svg-icons'
import { faGithub, faTelegram, faBluesky, faXTwitter } from '@fortawesome/free-brands-svg-icons'
import { SiGithub, SiForgejo, SiTelegram } from 'react-icons/si'
import { Mail, Smartphone } from 'lucide-react'
export default function Contact() {
const { t } = useTranslation();
@ -23,57 +23,66 @@ export default function Contact() {
];
const contactButtonLabels = [
t('contact.buttons.github'),
t('contact.buttons.telegram'),
t('contact.buttons.bluesky'),
t('contact.buttons.x'),
t('contact.buttons.phone'),
t('contact.buttons.email')
"ihatenodejs",
"aidan",
"p0ntu5",
"+1 802-416-9516",
"aidan@p0ntus.com",
];
const contactButtonHrefs = [
"https://github.com/ihatenodejs",
"https://git.p0ntus.com/aidan",
"https://t.me/p0ntu5",
"https://bsky.app/profile/aidxn.cc",
"https://x.com/ihatenodejs",
"tel:+18024169516",
"mailto:aidan@p0ntus.com"
];
const contactButtonIcons = [faGithub, faTelegram, faBluesky, faXTwitter, faPhone, faEnvelope];
const contactButtonIcons = [
<SiGithub key="github" />,
<SiForgejo key="forgejo" />,
<SiTelegram key="telegram" />,
<Smartphone key="smartphone" />,
<Mail key="mail" />
];
return (
<div className="min-h-screen flex flex-col">
<Header />
<main className="grow container mx-auto px-4 py-12">
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<div className="flex flex-col gap-4">
<div className="flex justify-center">
<Phone size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
<h1 className="text-4xl font-bold mt-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
{t('contact.title')}
</h1>
<div className="p-6 space-y-4">
</div>
<div className="flex flex-col gap-8 mt-8">
<div className="flex flex-wrap justify-center gap-3">
{contactButtonLabels.map((label, index) => (
<ContactButton
<Button
key={index}
label={label}
href={contactButtonHrefs[index]}
target="_blank"
variant="rounded"
icon={contactButtonIcons[index]}
className='mr-3'
/>
>
{label}
</Button>
))}
</div>
{sections.map((section, sectionIndex) => (
<div key={sectionIndex}>
<h2 className="text-2xl font-semibold mb-4 text-gray-200 mt-10">{section.title}</h2>
<div key={sectionIndex} className="flex flex-col gap-4">
<h2 className="text-2xl font-semibold text-gray-200">{section.title}</h2>
{section.texts.map((text, index) => (
<p key={index} className="text-gray-300 mb-4">{text}</p>
<p key={index} className="text-gray-300">{text}</p>
))}
</div>
))}
</div>
</div>
</main>
<Footer />
</div>

View file

@ -1,8 +1,7 @@
import Header from '@/components/Header'
import Footer from '@/components/Footer'
import { Link } from "lucide-react"
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
import { faBan } from "@fortawesome/free-solid-svg-icons"
import { TbCurrencyDollarOff } from "react-icons/tb";
import domains from "@/public/data/domains.json"
export default function Domains() {
@ -11,17 +10,16 @@ export default function Domains() {
<Header />
<main className="grow container mx-auto px-4 py-12">
<div className="max-w-2xl mx-auto flex flex-col items-center text-center">
<div className="mb-6 flex justify-center">
<div className="flex flex-col gap-4">
<div className="flex justify-center">
<Link size={60} />
</div>
<h1
className="text-4xl font-bold my-2 text-gray-200"
style={{ textShadow: "0 0 10px rgba(255, 255, 255, 0.5)" }}
>
<h1 className="text-4xl font-bold mt-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
My Domains
</h1>
</div>
<div className="mb-4 p-4 pt-8 flex flex-col items-center space-y-2">
<FontAwesomeIcon icon={faBan} className="text-red-500 text-xl" />
<TbCurrencyDollarOff size={26} className="text-red-500" />
<span className="text-red-500 font-medium text-center mt-1 mb-0">
These domains are not for sale.
</span>

View file

@ -8,12 +8,14 @@ export default function Manifesto() {
<Header />
<main className="grow container mx-auto px-4 py-12">
<div className="max-w-2xl mx-auto text-center">
<div className='mb-6 flex justify-center'>
<div className="flex flex-col gap-4">
<div className="flex justify-center">
<BookOpen size={60} />
</div>
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
<h1 className="text-4xl font-bold mt-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
Internet Manifesto
</h1>
</div>
<div className="px-6 pt-6">
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
1. Empathy and Understanding

View file

@ -42,9 +42,19 @@ export default function Home() {
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
<div className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
<div className="relative border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300 p-4">
<div className="absolute top-2 right-2">
<div className="flex items-center gap-1 bg-black bg-opacity-50 rounded-full px-2 py-1">
<div className="w-1 h-1 bg-red-400 rounded-full animate-pulse"></div>
<div className="text-white text-xs">
LIVE
</div>
</div>
</div>
<div className="flex justify-center items-center h-full">
<LastPlayed />
</div>
</div>
{mainSections.map((section, secIndex) => (
<section key={secIndex} className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
@ -62,9 +72,10 @@ export default function Home() {
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
<Button
href={'/contact'}
label={t('home.contact.button')}
icon={Mail}
/>
icon={<Mail size={16} />}
>
{t('home.contact.button')}
</Button>
</section>
<section id="donation" className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
@ -73,39 +84,44 @@ export default function Home() {
<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">
<Button
href={'https://unsilenced.org'}
label={t('home.donation.charities.unsilenced')}
icon={FaHandcuffs}
href="https://unsilenced.org"
icon={<FaHandcuffs />}
target="_blank"
/>
>
{t('home.donation.charities.unsilenced')}
</Button>
<Button
href={'https://drugpolicy.org'}
label={t('home.donation.charities.drugpolicy')}
icon={PillBottle}
href="https://drugpolicy.org"
icon={<PillBottle size={16} />}
target="_blank"
/>
>
{t('home.donation.charities.drugpolicy')}
</Button>
<Button
href={'https://www.aclu.org'}
label={t('home.donation.charities.aclu')}
icon={Scale}
href="https://www.aclu.org"
icon={<Scale size={16} />}
target="_blank"
/>
>
{t('home.donation.charities.aclu')}
</Button>
</div>
<h4 className="text-lg font-semibold mt-5 mb-2 text-gray-200">{t('home.donation.donate.title')}</h4>
<div className="grid grid-cols-1 md:grid-cols-2 md:text-sm gap-3">
<Button
href={'https://donate.stripe.com/6oEeWVcXs9L9ctW4gj'}
label={t('home.donation.donate.stripe')}
icon={CreditCard}
href="https://donate.stripe.com/6oEeWVcXs9L9ctW4gj"
icon={<CreditCard size={16} />}
target="_blank"
/>
>
{t('home.donation.donate.stripe')}
</Button>
<Button
href={'https://github.com/sponsors/ihatenodejs'}
label={t('home.donation.donate.github')}
icon={SiGithubsponsors}
href="https://github.com/sponsors/ihatenodejs"
icon={<SiGithubsponsors size={16} />}
target="_blank"
/>
>
{t('home.donation.donate.github')}
</Button>
</div>
</section>
</div>

View file

@ -1,24 +0,0 @@
import React from 'react';
import Link from 'next/link';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons';
interface BackButtonProps {
href: string;
label?: string;
}
const BackButton: React.FC<BackButtonProps> = ({ href, label = 'Back' }) => {
return (
<Link
href={href}
className="inline-flex items-center px-4 py-2 mt-4 text-white bg-gray-800 rounded-sm shadow-md transition-all duration-300 ease-in-out hover:bg-gray-700 hover:shadow-lg hover:-translate-y-0.5 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
aria-label={`Go back to ${label}`}
>
<FontAwesomeIcon icon={faArrowLeft} className="mr-2" />
{label}
</Link>
);
};
export default BackButton;

View file

@ -1,30 +1,40 @@
import React from 'react'
import Link from 'next/link'
import { cn } from '@/lib/utils'
interface ButtonProps {
href: string
label: string
icon?: React.ElementType
target?: string
variant?: "primary" | "rounded"
className?: string
icon?: React.ReactNode
children: React.ReactNode
}
const Button: React.FC<ButtonProps> = ({ href, label, icon, target, className }) => {
const Button: React.FC<ButtonProps> = ({ href, target, variant, className, icon, children }) => {
if (!variant || variant === "primary") {
return (
<Link
href={href}
className={className ? (
cn("inline-flex items-center bg-gray-800 text-white font-bold py-2 px-4 rounded-sm shadow-md transition-all duration-300 ease-in-out hover:bg-gray-700 hover:shadow-lg hover:-translate-y-0.5 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-gray-500", className)
) : (
"inline-flex items-center bg-gray-800 text-white font-bold py-2 px-4 rounded-sm shadow-md transition-all duration-300 ease-in-out hover:bg-gray-700 hover:shadow-lg hover:-translate-y-0.5 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-gray-500"
)}
className={`inline-flex items-center bg-gray-800 text-white font-bold py-2 px-4 rounded-sm shadow-md transition-all duration-300 ease-in-out hover:bg-gray-700 hover:shadow-lg hover:-translate-y-0.5 focus:outline-hidden focus:ring-2 focus:ring-offset-2 focus:ring-gray-500 gap-2 ${className}`}
target={target}
>
{icon && React.createElement(icon, { size: 20, className: "mr-2" })}
{label}
{icon}
{children}
</Link>
)
} else if (variant === "rounded") {
return (
<Link
href={href}
target={target}
rel={target === "_blank" ? "noopener noreferrer" : undefined}
className={`bg-gray-700 text-white px-4 py-2 rounded-full hover:bg-gray-600 transition-colors inline-flex items-center justify-center gap-2 whitespace-nowrap ${className}`}
>
{icon}
{children}
</Link>
)
}
}
export default Button

View file

@ -1,26 +0,0 @@
import { IconDefinition } from '@fortawesome/fontawesome-svg-core'
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
import Link from 'next/link';
interface ContactButtonProps {
href: string;
icon: IconDefinition;
label: string;
className?: string;
}
function ContactButton({ href, icon, label, className }: ContactButtonProps) {
return (
<Link
href={href}
target="_blank"
rel="noopener noreferrer"
className={`bg-gray-700 text-white px-4 py-2 rounded-full hover:bg-gray-600 transition-colors inline-flex items-center ${className}`}
>
<FontAwesomeIcon icon={icon} className="text-xl mr-2" />
{label}
</Link>
)
}
export default ContactButton;

View file

@ -0,0 +1,37 @@
"use client"
import type React from "react"
interface ScrollTxtProps {
text: string
className?: string
type?: 'artist' | 'track' | 'release'
}
const ScrollTxt: React.FC<ScrollTxtProps> = ({ text, className = "", type }) => {
const getTypeClass = (type?: string) => {
switch(type) {
case 'artist':
return 'text-white text-xs opacity-90 font-medium text-[8px]'
case 'track':
return 'text-white text-xs font-bold'
case 'release':
return 'text-white text-xs opacity-90 font-medium text-[8px]'
default:
return ''
}
}
const textClass = getTypeClass(type)
return (
<div className={`overflow-hidden ${className}`}>
<div className="whitespace-nowrap inline-block">
<span className={textClass}>{text}</span>
</div>
</div>
)
}
export default ScrollTxt

View file

@ -1,47 +0,0 @@
"use client"
import type React from "react"
import { useEffect, useRef, useState } from "react"
interface ScrollTxtProps {
text: string
className?: string
}
const ScrollTxt: React.FC<ScrollTxtProps> = ({ text, className = "" }) => {
const containerRef = useRef<HTMLDivElement>(null)
const textRef = useRef<HTMLDivElement>(null)
const [shouldScroll, setShouldScroll] = useState(false)
useEffect(() => {
if (containerRef.current && textRef.current) {
const containerWidth = containerRef.current.offsetWidth
const textWidth = textRef.current.offsetWidth
setShouldScroll(textWidth > containerWidth)
}
}, []) // Updated dependency array
return (
<div ref={containerRef} className={`overflow-hidden ${className}`}>
<div
ref={textRef}
className={`whitespace-nowrap inline-block ${shouldScroll ? "animate-marquee hover:pause" : ""}`}
>
{shouldScroll ? (
<>
<span>{text}</span>
<span className="mx-4">&bull;</span>
<span>{text}</span>
<span className="mx-4">&bull;</span>
<span>{text}</span>
</>
) : (
text
)}
</div>
</div>
)
}
export default ScrollTxt

View file

@ -1,13 +1,17 @@
"use client"
import type React from "react"
import { useEffect, useState, useCallback, useRef } from "react"
import { useEffect, useState, useCallback } from "react"
import Image from "next/image"
import { Music, ExternalLink, Disc, User, Loader2, AlertCircle } from "lucide-react"
import { TbDiscOff, TbDisc } from "react-icons/tb"
import Marquee from "react-fast-marquee"
import { Loader2, AlertCircle } from "lucide-react"
import { PiMusicNotesFill } from "react-icons/pi";
import { FaBluetoothB } from "react-icons/fa6";
import { IoBatteryFullSharp } from "react-icons/io5"
import { IoIosPlay } from "react-icons/io"
import { TbDiscOff } from "react-icons/tb"
import { Progress } from "@/components/ui/progress"
import Link from "@/components/objects/Link"
import ScrollTxt from "@/components/objects/MusicText"
interface Track {
track_name: string
@ -16,34 +20,6 @@ interface Track {
mbid?: string
}
const ScrollableText: React.FC<{ text: string; className?: string }> = ({ text, className = "" }) => {
const containerRef = useRef<HTMLDivElement>(null)
const [shouldScroll, setShouldScroll] = useState(false)
useEffect(() => {
if (containerRef.current) {
setShouldScroll(containerRef.current.scrollWidth > containerRef.current.clientWidth)
console.log("[i] text width checked: ", containerRef.current.scrollWidth, containerRef.current.clientWidth)
}
}, [containerRef])
if (shouldScroll) {
console.log("✅ scrolling is active")
return (
<Marquee gradientWidth={20} speed={20} pauseOnHover={true}>
<div className={className}>{text}</div>
<span className="mx-4 text-gray-400">&bull;</span>
</Marquee>
)
}
return (
<div ref={containerRef} className={`overflow-hidden ${className}`}>
<div className="whitespace-nowrap">{text}</div>
</div>
)
}
const NowPlaying: React.FC = () => {
const [track, setTrack] = useState<Track | null>(null)
const [coverArt, setCoverArt] = useState<string | null>(null)
@ -52,6 +28,9 @@ const NowPlaying: React.FC = () => {
const [error, setError] = useState<string | null>(null)
const [progress, setProgress] = useState(0)
const [steps, setSteps] = useState(0)
const [currentTime, setCurrentTime] = useState(new Date())
const [volume, setVolume] = useState(25)
const [screenOn, setScreenOn] = useState(true)
const updateProgress = useCallback((current: number, total: number, status: string) => {
setProgress(current)
@ -141,112 +120,260 @@ const NowPlaying: React.FC = () => {
fetchNowPlaying()
}, [fetchNowPlaying])
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date())
}, 1000)
return () => clearInterval(timer)
}, [])
const formatTime = (date: Date) => {
return date.toLocaleTimeString('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
})
}
const renderScreenContent = () => {
if (loading) {
console.log("[LastPlayed] Loading state rendered")
return (
<div className="mb-12">
<div className="flex items-center gap-2 mb-4">
<Loader2 className="animate-spin text-gray-200" size={24} />
<h2 className="text-2xl font-bold text-gray-200">Fetching music data...</h2>
</div>
<Progress value={steps > 0 ? (progress * 100) / steps : 0} className="mb-4" />
<div className="flex items-center justify-center space-x-2">
<p className="text-gray-200">
{loadingStatus} {steps > 0 && `(${progress}/${steps})`}
</p>
<div className="flex flex-col items-center justify-center h-full">
<Loader2 className="animate-spin text-white mb-4" size={32} />
<div className="text-white text-xs text-center px-4">
<div className="mb-2">{loadingStatus}</div>
<Progress value={steps > 0 ? (progress * 100) / steps : 0} className="h-1" />
</div>
</div>
)
}
if (error) {
console.log("[LastPlayed] Error state rendered")
return (
<div className="mb-12">
<h2 className="text-2xl font-bold mb-4 text-gray-200">Now Playing</h2>
<div className="flex items-center justify-center text-red-500">
<AlertCircle className="text-red-500 mr-2" size={24} />
<p>{error}</p>
</div>
<div className="flex flex-col items-center justify-center h-full">
<AlertCircle className="text-red-500 mb-4" size={32} />
<div className="text-red-500 text-xs text-center px-4">{error}</div>
</div>
)
}
if (!track) {
console.log("[LastPlayed] Hidden due to no track data")
return (
<div className="mb-12">
<div className="flex items-center gap-2 mb-4">
<TbDiscOff className="text-gray-200" size={24} />
<h2 className="text-2xl font-bold text-gray-200">Nothing&apos;s playing right now</h2>
<div className="flex flex-col items-center justify-center h-full">
<TbDiscOff className="text-gray-400 mb-4" size={32} />
<div className="text-gray-400 text-xs text-center px-4">
Nothing playing
</div>
<div className="flex items-center justify-center">
<p>Can you believe it? I&apos;m not listening to anything on ListenBrainz right now! If you&apos;re in the mood, feel free to check out my <Link href="https://listenbrainz.org/user/p0ntus" target="_blank" rel="noopener noreferrer">ListenBrainz</Link>.</p>
<div className="text-gray-500 text-xs text-center px-4 mt-2">
Check my <Link href="https://listenbrainz.org/user/p0ntus" target="_blank" rel="noopener noreferrer" className="text-blue-400">ListenBrainz</Link>
</div>
</div>
)
}
console.log("[LastPlayed] Rendered track:", track.track_name)
// normal state
const currentTrack = track!;
return (
<div className="mb-12">
<div className="flex items-center gap-2 mb-4">
<TbDisc className="animate-spin text-gray-200" size={24} />
<h2 className="text-2xl font-bold text-gray-200">Now Playing</h2>
</div>
<div className="now-playing flex items-center">
{coverArt ? (
<div className="relative w-26 h-26 md:w-40 md:h-40 rounded-lg mr-4 flex-shrink-0">
<Image
src={coverArt || ""}
alt={track.track_name}
fill
sizes="96px"
style={{ objectFit: "cover" }}
className="rounded-lg"
/>
</div>
) : (
<div className="w-26 h-26 md:w-40 md:h-40 bg-gray-200 rounded-lg mr-4 flex items-center justify-center flex-shrink-0">
<Music size={48} className="text-gray-400" />
</div>
)}
<div className="flex-grow min-w-0 overflow-hidden">
<div className="flex items-center space-x-2 font-bold text-lg mb-1">
<Music size={16} className="text-gray-200 flex-shrink-0" />
<ScrollableText text={track.track_name} className="text-gray-200" />
</div>
{track.release_name && (
<div className="flex items-center space-x-2 mb-1">
<Disc size={16} className="text-gray-300 flex-shrink-0" />
<ScrollableText text={track.release_name} className="text-gray-300" />
</div>
)}
<div className="flex items-center space-x-2 mb-2">
<User size={16} className="text-gray-300 flex-shrink-0" />
<ScrollableText text={track.artist_name} className="text-gray-300" />
</div>
<>
<a
href={track.mbid ? `https://musicbrainz.org/release/${track.mbid}` : `https://listenbrainz.org/user/p0ntus`}
href={currentTrack.mbid ? `https://musicbrainz.org/release/${currentTrack.mbid}` : `https://listenbrainz.org/user/p0ntus`}
target="_blank"
rel="noopener noreferrer"
className="text-blue-400 flex items-center mt-1 hover:text-blue-300 transition-colors duration-200"
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%)'}}
>
<ExternalLink size={16} className="mr-1 flex-shrink-0" />
<span>View on MusicBrainz</span>
<div className="text-center leading-none">
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" className="-my-0.5" />
<ScrollTxt text={currentTrack.track_name} type="track" className="-my-0.5" />
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" className="-mt-1.5 mb-0.5" />}
</div>
</a>
{/* Album art */}
<div className="relative w-full aspect-square">
{coverArt ? (
<Image
src={coverArt}
alt={currentTrack.track_name}
fill
style={{ objectFit: "cover" }}
/>
) : (
<div className="w-full h-full bg-gray-700 flex items-center justify-center">
<PiMusicNotesFill size={74} className="text-gray-400" />
</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>
</>
)
}
return (
<div className="flex justify-center items-center">
<div className="relative w-52 h-95 bg-[#D4C29A] rounded-xs shadow-2xl border border-[#BFAF8A]">
{/* Volume buttons */}
<div className="absolute -left-[2.55px] top-8 rounded-l w-[1.75px] flex flex-col z-0">
<div className="h-8 bg-[#BFAF8A] border-b border-[#A09070] rounded-l cursor-pointer" onClick={() => setVolume(v => Math.min(100, v + 5))}></div> {/* up */}
<div className="h-12 bg-[#A09070] translate-x-[0.65px] -my-[0.85px]"></div> {/* play/pause */}
<div className="h-8 bg-[#BFAF8A] border-t border-[#A09070] rounded-l cursor-pointer" onClick={() => setVolume(v => Math.max(0, v - 5))}></div> {/* down */}
</div>
{/* Top power button */}
<div className="absolute right-1 -top-[3px] w-9 h-[3px] bg-[#BFAF8A] rounded-t-2xl cursor-pointer" onClick={() => setScreenOn(prev => !prev)}></div>
{/* White bezel (fuses screen+home btn) */}
<div className="absolute inset-2 bg-white rounded-sm overflow-hidden flex flex-col">
{/* 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="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>
<div className="flex items-center gap-0.5 ml-auto ">
<IoIosPlay size={14} className="text-white" />
<IoBatteryFullSharp size={18} className="text-white" />
</div>
</div>
</div>
)}
{screenOn ? renderScreenContent() : (
<div className="w-full h-full bg-black"></div>
)}
{/* Player controls and seekbar */}
{screenOn && track && (
<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>
)}
</div>
{/* Home button */}
<div className="flex justify-center py-2">
<div className="w-8 h-8 bg-white rounded-full border border-gray-300 shadow flex items-center justify-center">
<div className="w-4 h-4 border-1 border-[#D4C29A] rounded-full"></div>
</div>
</div>
<div className="flex flex-col items-center gap-2 mt-6">
<div className="flex items-center gap-1">
<div className="w-2 h-2 bg-red-400 rounded-full"></div>
<p className="text-gray-200 text-sm">
<span className="font-bold text-red-400">LIVE</span> data provided by <Link href="https://listenbrainz.org" target="_blank" rel="noopener noreferrer">ListenBrainz</Link>
</p>
</div>
<p className="text-gray-200 text-sm">
Last updated: {new Date().toLocaleString()}
</p>
</div>
</div>
)

View file

@ -9,38 +9,33 @@
"lint": "next lint"
},
"dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.7.2",
"@fortawesome/free-brands-svg-icons": "^6.7.2",
"@fortawesome/free-solid-svg-icons": "^6.7.2",
"@fortawesome/react-fontawesome": "^0.2.2",
"@radix-ui/react-progress": "^1.1.6",
"@radix-ui/react-progress": "^1.1.7",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"geist": "^1.4.2",
"i18next": "^24.2.3",
"i18next-browser-languagedetector": "^8.1.0",
"i18next-browser-languagedetector": "^8.2.0",
"lucide-react": "^0.485.0",
"next": "^15.3.2",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-fast-marquee": "^1.6.5",
"react-i18next": "^15.5.1",
"next": "^15.4.5",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-i18next": "^15.6.1",
"react-icons": "^5.5.0",
"tailwind-merge": "^3.2.0",
"tailwind-merge": "^3.3.1",
"tailwindcss-animate": "^1.0.7",
"tw-animate-css": "^1.2.9"
"tw-animate-css": "^1.3.6"
},
"devDependencies": {
"@eslint/eslintrc": "^3.3.1",
"@tailwindcss/postcss": "^4.1.6",
"@types/node": "^20.17.46",
"@types/react": "^19.1.3",
"@types/react-dom": "^19.1.3",
"eslint": "^9.26.0",
"@tailwindcss/postcss": "^4.1.11",
"@types/node": "^20.19.9",
"@types/react": "^19.1.9",
"@types/react-dom": "^19.1.7",
"eslint": "^9.32.0",
"eslint-config-next": "15.1.3",
"postcss": "^8.5.3",
"tailwindcss": "^4.1.6",
"typescript": "^5.8.3"
"postcss": "^8.5.6",
"tailwindcss": "^4.1.11",
"typescript": "^5.9.2"
},
"trustedDependencies": [
"sharp",

View file

@ -1,20 +1,20 @@
{
"home": {
"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, and Node.js.",
"I primarily focus on Linux system administration with a few servers I run for myself and others. I enjoy working on web development projects on the side, most of which are Unlicensed/CC0.",
"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.",
"My favorite projects and hobbies include 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!",
"When I'm not programming, I can be found re-flashing my phone with a new custom ROM and jumping between projects."
],
"whatIDo": [
"I'm at my best when I'm doing system administration, which is what I'd say I have the most experience and familiarity with.",
"I host a variety of public-access services and websites on my VPS, most of which can be found on my \"Domains\" page with a short description.",
"My biggest project is LibreCloud, a cloud services provider which I self-host and maintain. It features most services you would find from large companies like Google, although everything is free and open-source.",
"I frequently write and work on a website hosted on a public Linux server, known as a \"tilde.\""
"I'm at my best when I'm doing system administration and development in TypeScript. I frequently implement AI into my workflow.",
"I manage three servers, including a mailserver (against my better judgement). I'm also crazy enough to self-host LLMs running on CPU.",
"My biggest project is p0ntus, a cloud services provider which I self-host and maintain. It features most services you would find from large companies like Google, although everything is free and open-source."
],
"whereYouAre": [
"I am not here to brag about my accomplishments or plug my cool SaaS product. 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.",
"In a technical sense, this site is hosted on my NY1 dedicated server, hosted by ColoCrossing out of Buffalo, New York."
"In a technical sense, this site is hosted on my dedicated server hosted in Buffalo, New York by ColoCrossing."
],
"sections": {
"whoIAm": "Who I am",
@ -23,7 +23,7 @@
},
"contact": {
"title": "Send me a message",
"description": "Feel free to reach out for feedback, collaborations, or just a hello :)",
"description": "Feel free to reach out for feedback, collaborations, or just a hello! I aim to answer all of my messages in a timely fashion, but please have patience.",
"button": "Contact Me"
},
"donation": {
@ -67,14 +67,6 @@
"If you need to get in touch with me, please send me a message on Telegram or an email. I will provide my actual phone number if you have a valid reason."
]
}
},
"buttons": {
"github": "ihatenodejs",
"telegram": "@p0ntu5",
"x": "@ihatenodejs",
"bluesky": "@aidxn.cc",
"phone": "(802) 416-9516",
"email": "aidan@p0ntus.com"
}
},
"about": {
@ -90,6 +82,7 @@
"projects": [
"I have worked on countless projects over the past five years, for the most part. I started learning to code with Python when I was seven and my interest has only evolved from there. I got into web development due to my uncle, who taught my how to write my first lines of HTML.",
"Recently, I have been involved in developing several projects, especially with TypeScript, which is my new favorite language as of a year ago. My biggest project currently is p0ntus, a free service provider for privacy-focused individuals.",
"You will also come to find that I have an addiction to Docker! Almost every project I've made is able to be run in Docker.",
"Me and my developer friends operate an organization called ABOCN, where we primarily maintain a Telegram bot called Kowalski. You can find it on Telegram as @KowalskiNodeBot.",
"I have learned system administration from the past three years of learning Linux for practical use and fun. I currently operate four servers running in the cloud, ran out of Canada, Germany, and the United States.",
"I own a channel called PontusHub on Telegram, where I post updates about my projects, along with commentary and info about my projects related to the Android rooting community."
@ -108,8 +101,9 @@
"I also have a Google Pixel 3a XL (bonito) which I use as a secondary device. It runs LineageOS 22.2 and is also rooted with KernelSU-Next."
],
"Laptops": [
"I use a Lenovo Thinkpad T470s with NixOS unstable as my daily driver. I've had it for about a year now, and it's been a great experience. At the time of writing, I am using KDE Plasma as my desktop environment.",
"I also own two MacBook Airs (2015 and 2013 base model) and a HP Chromebook used as a secondary devices. The 2013 runs unsupported macOS Sequoia Beta, the 2015 runs Xubuntu, and the Chromebook runs Arch Linux."
"I currently daily-drive with a 16-inch MacBook Pro with an M4 Max, 64GB of memory, 2TB of storage, 16 core CPU, and a 40 core GPU.",
"I use a Lenovo Thinkpad T470s with macOS Sequoia (using OpenCore) as my \"side piece,\" if you will. I've had it for about a year now, and it's been a great experience.",
"I also own two MacBook Airs (2015 and 2013 base models) and an HP Chromebook, used as secondary devices. The 2013 runs unsupported macOS Sequoia Beta, the 2015 runs Xubuntu, and the Chromebook runs Arch Linux."
]
},
"contributions": [

View file

@ -6,7 +6,6 @@ const config: Config = {
content: [
"app/**/*.{ts,tsx}",
"components/**/*.{ts,tsx}",
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
"./components/**/*.{js,ts,jsx,tsx,mdx}",
"./app/**/*.{js,ts,jsx,tsx,mdx}",
"*.{js,ts,jsx,tsx,mdx}",