Compare commits
1 commit
main
...
dependabot
Author | SHA1 | Date | |
---|---|---|---|
![]() |
72b5fa7b85 |
54 changed files with 2645 additions and 4069 deletions
30
.gitea/workflows/lint.yml
Normal file
30
.gitea/workflows/lint.yml
Normal file
|
@ -0,0 +1,30 @@
|
|||
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
|
41
.gitea/workflows/push.yml
Normal file
41
.gitea/workflows/push.yml
Normal file
|
@ -0,0 +1,41 @@
|
|||
# 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 }}
|
23
Dockerfile
23
Dockerfile
|
@ -1,10 +1,16 @@
|
|||
FROM oven/bun:latest AS base
|
||||
FROM node:23-alpine AS base
|
||||
|
||||
FROM base AS deps
|
||||
RUN apk add --no-cache libc6-compat
|
||||
WORKDIR /app
|
||||
|
||||
COPY package.json ./
|
||||
RUN bun install
|
||||
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* .npmrc* ./
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
|
||||
elif [ -f package-lock.json ]; then npm ci; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /app
|
||||
|
@ -13,7 +19,12 @@ COPY . .
|
|||
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN bun run build
|
||||
RUN \
|
||||
if [ -f yarn.lock ]; then yarn run build; \
|
||||
elif [ -f package-lock.json ]; then npm run build; \
|
||||
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
|
||||
else echo "Lockfile not found." && exit 1; \
|
||||
fi
|
||||
|
||||
FROM base AS runner
|
||||
WORKDIR /app
|
||||
|
@ -21,8 +32,8 @@ WORKDIR /app
|
|||
ENV NODE_ENV=production
|
||||
ENV NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
RUN groupadd --system --gid 1001 nodejs
|
||||
RUN useradd --system --uid 1001 nextjs
|
||||
RUN addgroup --system --gid 1001 nodejs
|
||||
RUN adduser --system --uid 1001 nextjs
|
||||
|
||||
COPY --from=builder /app/public ./build/public
|
||||
|
||||
|
|
39
README.md
39
README.md
|
@ -1,19 +1,42 @@
|
|||
# aidxnCC
|
||||
|
||||
[](http://unlicense.org/)
|
||||
[](https://git.pontusmail.org/aidan/aidxnCC/actions/?workflow=push.yml)
|
||||
[](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 with Docker
|
||||
## Deploy
|
||||
|
||||
Docker is the easiest way to deploy aidxnCC. There are two example `docker-compose.yml` files for you to use.
|
||||
### Vercel
|
||||
|
||||
1. `docker-compose.yml` - Default, exposed on port 3000
|
||||
2. `docker-compose.nginx.yml` - Helpful for NGINX Proxy Manager usage w/ Docker networks
|
||||
[](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)
|
||||
|
||||
Just create a `.env` file with the below variables, run `docker compose -d --build`, and you'll be all set.
|
||||
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.
|
||||
|
||||
## 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
|
||||
|
||||
|
@ -26,9 +49,3 @@ Just create a `.env` file with the below variables, run `docker compose -d --bui
|
|||
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.
|
||||
|
||||
## 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 :)
|
||||
|
|
|
@ -7,8 +7,7 @@ import Button from '@/components/objects/Button'
|
|||
import FeaturedRepos from '@/components/widgets/FeaturedRepos'
|
||||
import Image from 'next/image'
|
||||
import { useState } from 'react'
|
||||
import { User } from 'lucide-react'
|
||||
import { SiGoogle } from 'react-icons/si'
|
||||
import { User, Smartphone } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
|
@ -33,19 +32,19 @@ export default function About() {
|
|||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="w-full">
|
||||
<div className="my-12 text-center">
|
||||
<div className="flex justify-center mb-6">
|
||||
<User size={60} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">{t('about.title')}</h1>
|
||||
<main className="text-center py-12">
|
||||
<div className='mb-6 flex justify-center'>
|
||||
<User 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)' }}>
|
||||
{t('about.title')}
|
||||
</h1>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 p-4">
|
||||
{mainStrings.map((section, index) => {
|
||||
if (mainSections[index] === t('about.sections.featuredProjects')) {
|
||||
return (
|
||||
<section key={index} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg lg:col-span-2 hover:border-gray-600 transition-colors duration-300">
|
||||
<section key={index} className="p-8 border-2 border-gray-700 rounded-lg col-span-2 hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">{mainSections[index]}</h2>
|
||||
{section.map((text, index) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
|
@ -57,19 +56,19 @@ export default function About() {
|
|||
)
|
||||
} else if (mainSections[index] === t('about.sections.contributions')) {
|
||||
return (
|
||||
<section key={index} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<section key={index} className="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">{mainSections[index]}</h2>
|
||||
{section.map((text, index) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
{text.split(/(ihatenodejs|p0ntus git|aidan)/).map((part, i) => {
|
||||
{text.split(/(ihatenodejs|LibreCloud Git|aidan)/).map((part, i) => {
|
||||
if (part === 'ihatenodejs') {
|
||||
return <Link key={i} href="https://github.com/ihatenodejs/">ihatenodejs</Link>
|
||||
return <Link key={i} href="https://github.com/ihatenodejs/">GitHub</Link>
|
||||
}
|
||||
if (part === 'p0ntus git') {
|
||||
return <Link key={i} href="https://git.p0ntus.com/">p0ntus git</Link>
|
||||
if (part === 'LibreCloud Git') {
|
||||
return <Link key={i} href="https://git.pontusmail.org/">LibreCloud Git</Link>
|
||||
}
|
||||
if (part === 'aidan') {
|
||||
return <Link key={i} href="https://git.p0ntus.com/aidan/">aidan</Link>
|
||||
return <Link key={i} href="https://git.pontusmail.org/aidan/">aidan</Link>
|
||||
}
|
||||
return part
|
||||
})}
|
||||
|
@ -77,19 +76,18 @@ export default function About() {
|
|||
))}
|
||||
{!imageError && (
|
||||
<div className="flex flex-col justify-center items-center w-full mt-4 gap-4">
|
||||
<Image
|
||||
src="https://github-readme-stats.vercel.app/api?username=ihatenodejs&theme=dark&show_icons=true&hide_border=true&count_private=true"
|
||||
<Image
|
||||
src="https://github-readme-stats.vercel.app/api?username=ihatenodejs&theme=dark&show_icons=true&hide_border=true&count_private=true"
|
||||
alt="ihatenodejs's Stats"
|
||||
width={420}
|
||||
width={420}
|
||||
height={200}
|
||||
onError={() => setImageError(true)}
|
||||
loading="eager"
|
||||
priority
|
||||
unoptimized
|
||||
className="max-w-full h-auto"
|
||||
/>
|
||||
<Image
|
||||
src="https://github-readme-stats.vercel.app/api/top-langs/?username=ihatenodejs&theme=dark&show_icons=true&hide_border=true&layout=compact"
|
||||
<Image
|
||||
src="https://github-readme-stats.vercel.app/api/top-langs/?username=ihatenodejs&theme=dark&show_icons=true&hide_border=true&layout=compact"
|
||||
alt="ihatenodejs's Top Languages"
|
||||
width={300}
|
||||
height={200}
|
||||
|
@ -97,7 +95,6 @@ export default function About() {
|
|||
loading="eager"
|
||||
priority
|
||||
unoptimized
|
||||
className="max-w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
@ -105,54 +102,31 @@ export default function About() {
|
|||
)
|
||||
} else if (mainSections[index] === t('about.sections.devices')) {
|
||||
return (
|
||||
<section key={index} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<section key={index} className="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">{mainSections[index]}</h2>
|
||||
{Object.entries(section).map(([key, value], index) => (
|
||||
<div key={index}>
|
||||
<h3 className={cn("text-xl font-semibold mb-2 text-gray-200", key === "Laptops" && "mt-4")}>{key}</h3>
|
||||
<h3 className={cn("text-xl font-semibold mb-2 text-gray-200", key === "Laptop" && "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|Xubuntu)/).map((part, i) => {
|
||||
{text.split(/(KernelSU-Next|LineageOS microG)/).map((part, i) => {
|
||||
if (part === 'KernelSU-Next') {
|
||||
return <Link key={i} href="https://github.com/KernelSU-Next/KernelSU-Next">KernelSU-Next</Link>
|
||||
}
|
||||
if (part === 'LineageOS 22.2') {
|
||||
return <Link key={i} href="https://wiki.lineageos.org/devices/bonito/">LineageOS 22.2</Link>
|
||||
}
|
||||
if (part === 'Android 16') {
|
||||
return <Link key={i} href="https://developer.android.com/about/versions/16/get">Android 16</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>
|
||||
if (part === 'LineageOS microG') {
|
||||
return <Link key={i} href="https://lineage.microg.org/">LineageOS microG</Link>
|
||||
}
|
||||
return part
|
||||
})}
|
||||
</p>
|
||||
))}
|
||||
{key === "Mobile Devices" && (
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3 mt-4">
|
||||
<Button
|
||||
href="/device/komodo"
|
||||
icon={<SiGoogle />}
|
||||
>
|
||||
Pixel 9 Pro XL
|
||||
</Button>
|
||||
<Button
|
||||
href="/device/cheetah"
|
||||
icon={<SiGoogle />}
|
||||
>
|
||||
Pixel 7 Pro
|
||||
</Button>
|
||||
<Button
|
||||
href="/device/bonito"
|
||||
icon={<SiGoogle />}
|
||||
>
|
||||
Pixel 3a XL
|
||||
</Button>
|
||||
</div>
|
||||
{key === "Phone" && (
|
||||
<Button
|
||||
href="/phone"
|
||||
label="My Phone"
|
||||
icon={Smartphone}
|
||||
className="mt-4"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
@ -160,22 +134,16 @@ export default function About() {
|
|||
)
|
||||
} else if (mainSections[index] === t('about.sections.hobbies')) {
|
||||
return (
|
||||
<section key={index} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<section key={index} className="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">{mainSections[index]}</h2>
|
||||
{section.map((text, index) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
{text.split(/(my Forgejo server|my phone|AfC|OnlyNano)/).map((part, i) => {
|
||||
if (part === 'my Forgejo server') {
|
||||
return <Link key={i} href="https://git.p0ntus.com/">my Forgejo server</Link>
|
||||
{text.split(/(my Gitea instance|my phone)/).map((part, i) => {
|
||||
if (part === 'my Gitea instance') {
|
||||
return <Link key={i} href="https://git.pontusmail.org/">my Gitea instance</Link>
|
||||
}
|
||||
if (part === 'my phone') {
|
||||
return <Link key={i} href="/device/cheetah">my phone</Link>
|
||||
}
|
||||
if (part === 'AfC') {
|
||||
return <Link key={i} href="https://en.wikipedia.org/wiki/Wikipedia:WikiProject_Articles_for_creation">AfC</Link>
|
||||
}
|
||||
if (part === 'OnlyNano') {
|
||||
return <Link key={i} href="https://en.wikipedia.org/wiki/User:OnlyNano">OnlyNano</Link>
|
||||
return <Link key={i} href="/phone">my phone</Link>
|
||||
}
|
||||
return part
|
||||
})}
|
||||
|
@ -185,25 +153,13 @@ export default function About() {
|
|||
)
|
||||
} else if (mainSections[index] === t('about.sections.projects')) {
|
||||
return (
|
||||
<section key={index} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<section key={index} className="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">{mainSections[index]}</h2>
|
||||
{section.map((text, index) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
{text.split(/(p0ntus|PontusHub|ABOCN|Kowalski|@KowalskiNodeBot)/).map((part, i) => {
|
||||
if (part === 'p0ntus') {
|
||||
return <Link key={i} href="https://p0ntus.com/">p0ntus</Link>
|
||||
}
|
||||
if (part === 'PontusHub') {
|
||||
return <Link key={i} href="https://t.me/PontusHub">PontusHub</Link>
|
||||
}
|
||||
if (part === 'ABOCN') {
|
||||
return <Link key={i} href="https://github.com/abocn">ABOCN</Link>
|
||||
}
|
||||
if (part === 'Kowalski') {
|
||||
return <Link key={i} href="https://github.com/abocn/TelegramBot">Kowalski</Link>
|
||||
}
|
||||
if (part === '@KowalskiNodeBot') {
|
||||
return <Link key={i} href="https://t.me/KowalskiNodeBot">@KowalskiNodeBot</Link>
|
||||
{text.split(/(LibreCloud)/).map((part, i) => {
|
||||
if (part === 'LibreCloud') {
|
||||
return <Link key={i} href="https://librecloud.cc/">LibreCloud</Link>
|
||||
}
|
||||
return part
|
||||
})}
|
||||
|
@ -213,7 +169,7 @@ export default function About() {
|
|||
)
|
||||
} else {
|
||||
return (
|
||||
<section key={index} className="p-4 sm:p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<section key={index} className="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">{mainSections[index]}</h2>
|
||||
{section.map((text, index) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
|
|
|
@ -1,445 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import { useState, useEffect } from 'react'
|
||||
import { SiClaude } from 'react-icons/si'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
Line,
|
||||
BarChart,
|
||||
Bar,
|
||||
PieChart,
|
||||
Pie,
|
||||
Cell,
|
||||
XAxis,
|
||||
YAxis,
|
||||
CartesianGrid,
|
||||
Tooltip,
|
||||
Legend,
|
||||
ResponsiveContainer,
|
||||
Area,
|
||||
AreaChart,
|
||||
ComposedChart,
|
||||
} from 'recharts'
|
||||
|
||||
interface ModelBreakdown {
|
||||
modelName: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
cost: number
|
||||
}
|
||||
|
||||
interface DailyData {
|
||||
date: string
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
totalTokens: number
|
||||
totalCost: number
|
||||
modelsUsed: string[]
|
||||
modelBreakdowns: ModelBreakdown[]
|
||||
}
|
||||
|
||||
interface CCData {
|
||||
daily: DailyData[]
|
||||
totals: {
|
||||
inputTokens: number
|
||||
outputTokens: number
|
||||
cacheCreationTokens: number
|
||||
cacheReadTokens: number
|
||||
totalCost: number
|
||||
totalTokens: number
|
||||
}
|
||||
}
|
||||
|
||||
const COLORS = ['#c15f3c', '#b1ada1', '#f4f3ee', '#c15f3c', '#b1ada1', '#f4f3ee']
|
||||
|
||||
export default function AI() {
|
||||
const [data, setData] = useState<CCData | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedMetric, setSelectedMetric] = useState<'cost' | 'tokens'>('cost')
|
||||
|
||||
useEffect(() => {
|
||||
fetch('/data/cc.json')
|
||||
.then(res => {
|
||||
if (!res.ok) throw new Error('Failed to fetch data')
|
||||
return res.json()
|
||||
})
|
||||
.then(data => {
|
||||
setData(data)
|
||||
setLoading(false)
|
||||
})
|
||||
.catch(err => {
|
||||
setError(err.message)
|
||||
setLoading(false)
|
||||
})
|
||||
}, [])
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<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="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>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="flex-1 flex items-center justify-center">
|
||||
<div className="text-red-400">Error loading data: {error}</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const modelUsageData = data.daily.reduce((acc, day) => {
|
||||
day.modelBreakdowns.forEach(model => {
|
||||
const existing = acc.find(m => m.name === model.modelName)
|
||||
if (existing) {
|
||||
existing.value += model.cost
|
||||
} else {
|
||||
acc.push({ name: model.modelName, value: model.cost })
|
||||
}
|
||||
})
|
||||
return acc
|
||||
}, [] as { name: string; value: number }[])
|
||||
.sort((a, b) => b.value - a.value)
|
||||
|
||||
const tokenTypeData = [
|
||||
{ name: 'Input', value: data.totals.inputTokens },
|
||||
{ name: 'Output', value: data.totals.outputTokens },
|
||||
{ name: 'Cache Creation', value: data.totals.cacheCreationTokens },
|
||||
{ name: 'Cache Read', value: data.totals.cacheReadTokens },
|
||||
]
|
||||
|
||||
const dailyTrendData = data.daily.map(day => ({
|
||||
date: new Date(day.date + 'T00:00:00').toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
|
||||
cost: day.totalCost,
|
||||
tokens: day.totalTokens / 1000000,
|
||||
inputTokens: day.inputTokens / 1000,
|
||||
outputTokens: day.outputTokens / 1000,
|
||||
cacheTokens: (day.cacheCreationTokens + day.cacheReadTokens) / 1000000,
|
||||
}))
|
||||
|
||||
const formatCurrency = (value: number) => `$${value.toFixed(2)}`
|
||||
const formatTokens = (value: number) => `${value.toFixed(1)}M`
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<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="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 hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Cost</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">${data.totals.totalCost.toFixed(2)}</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Total Tokens</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">{(data.totals.totalTokens / 1000000).toFixed(1)}M</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Days Active</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">{data.daily.length}</p>
|
||||
</div>
|
||||
<div className="p-6 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<h3 className="text-sm font-medium text-gray-400 mb-2">Avg Daily Cost</h3>
|
||||
<p className="text-3xl font-bold text-[#c15f3c]">${(data.totals.totalCost / data.daily.length).toFixed(2)}</p>
|
||||
</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 hover:border-gray-600 transition-colors duration-300">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">Daily Usage Trend</h2>
|
||||
<div className="flex gap-2 mb-4">
|
||||
<button
|
||||
onClick={() => setSelectedMetric('cost')}
|
||||
className={`px-3 py-1 rounded ${selectedMetric === 'cost' ? 'bg-[#c15f3c] text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
>
|
||||
Cost
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setSelectedMetric('tokens')}
|
||||
className={`px-3 py-1 rounded ${selectedMetric === 'tokens' ? 'bg-[#c15f3c] text-white' : 'bg-gray-700 text-gray-300'}`}
|
||||
>
|
||||
Tokens
|
||||
</button>
|
||||
</div>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<AreaChart data={dailyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9ca3af" />
|
||||
<YAxis
|
||||
stroke="#9ca3af"
|
||||
tickFormatter={selectedMetric === 'cost' ? formatCurrency : formatTokens}
|
||||
/>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => selectedMetric === 'cost' ? formatCurrency(value) : formatTokens(value)}
|
||||
/>
|
||||
<Area
|
||||
type="monotone"
|
||||
dataKey={selectedMetric === 'cost' ? 'cost' : 'tokens'}
|
||||
stroke="#c15f3c"
|
||||
fill="#c15f3c"
|
||||
fillOpacity={0.3}
|
||||
/>
|
||||
</AreaChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
|
||||
<section className="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">Model Usage Distribution</h2>
|
||||
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<PieChart>
|
||||
<Pie
|
||||
data={modelUsageData}
|
||||
cx="50%"
|
||||
cy="50%"
|
||||
innerRadius={60}
|
||||
outerRadius={100}
|
||||
fill="#8884d8"
|
||||
paddingAngle={2}
|
||||
dataKey="value"
|
||||
>
|
||||
{modelUsageData.map((entry, index) => (
|
||||
<Cell key={`cell-${index}`} fill={COLORS[index % COLORS.length]} />
|
||||
))}
|
||||
</Pie>
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151', borderRadius: '8px' }}
|
||||
formatter={(value: number) => formatCurrency(value)}
|
||||
labelStyle={{ color: '#fff' }}
|
||||
itemStyle={{ color: '#fff' }}
|
||||
/>
|
||||
</PieChart>
|
||||
</ResponsiveContainer>
|
||||
<div className="flex flex-col justify-center space-y-3">
|
||||
{modelUsageData.map((model, index) => {
|
||||
const percentage = ((model.value / data.totals.totalCost) * 100).toFixed(1)
|
||||
return (
|
||||
<div key={index} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-3 h-3 rounded-full"
|
||||
style={{ backgroundColor: COLORS[index % COLORS.length] }}
|
||||
/>
|
||||
<span className="text-gray-300 font-medium text-xs">{model.name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-gray-400 text-sm">{percentage}%</span>
|
||||
<span className="text-gray-200 font-semibold">${model.value.toFixed(2)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
<div className="pt-3 mt-3 border-t border-gray-700">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-gray-400">Total Models Used</span>
|
||||
<span className="text-gray-200 font-bold">{modelUsageData.length}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mt-2">
|
||||
<span className="text-gray-400">Most Used</span>
|
||||
<span className="text-gray-200 font-bold text-xs">
|
||||
{modelUsageData[0]?.name}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="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">Token Type Breakdown</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<BarChart data={tokenTypeData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="name" stroke="#9ca3af" />
|
||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${(value / 1000000).toFixed(0)}M`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: 'rgba(31, 41, 55)', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => `${(value / 1000000).toFixed(2)}M tokens`}
|
||||
/>
|
||||
<Bar
|
||||
dataKey="value"
|
||||
fill="#b1ada1"
|
||||
/>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
|
||||
<section className="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">Daily Token Composition</h2>
|
||||
<ResponsiveContainer width="100%" height={300}>
|
||||
<ComposedChart data={dailyTrendData}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#374151" />
|
||||
<XAxis dataKey="date" stroke="#9ca3af" />
|
||||
<YAxis stroke="#9ca3af" tickFormatter={(value) => `${value}K`} />
|
||||
<Tooltip
|
||||
contentStyle={{ backgroundColor: '#1f2937', border: '1px solid #374151' }}
|
||||
formatter={(value: number) => `${value.toFixed(1)}K tokens`}
|
||||
/>
|
||||
<Legend />
|
||||
<Bar dataKey="inputTokens" stackId="a" fill="#c15f3c" name="Input (K)" />
|
||||
<Bar dataKey="outputTokens" stackId="a" fill="#b1ada1" name="Output (K)" />
|
||||
<Line type="monotone" dataKey="cacheTokens" stroke="#f4f3ee" name="Cache (M)" strokeWidth={2} />
|
||||
</ComposedChart>
|
||||
</ResponsiveContainer>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<section className="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">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>
|
||||
{data.daily.slice(-5).reverse().map((day, index) => (
|
||||
<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 + 'T00:00:00').toLocaleDateString()}</td>
|
||||
<td className="py-2 px-4 text-gray-300">
|
||||
{day.modelsUsed.join(', ')}
|
||||
</td>
|
||||
<td className="py-2 px-4 text-gray-300">{(day.totalTokens / 1000000).toFixed(2)}M</td>
|
||||
<td className="py-2 px-4 text-[#c15f3c] font-semibold">${day.totalCost.toFixed(2)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
import { Bot } from 'lucide-react'
|
||||
import Link from '@/components/objects/Link'
|
||||
import type { AITool } from '../types'
|
||||
|
||||
interface AIStackProps {
|
||||
tools: AITool[]
|
||||
}
|
||||
|
||||
export default function AIStack({ tools }: AIStackProps) {
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case 'primary': return 'text-green-400 border-green-400 bg-green-400/10'
|
||||
case 'active': return 'text-blue-400 border-blue-400 bg-blue-400/10'
|
||||
case 'occasional': return 'text-yellow-400 border-yellow-400 bg-yellow-400/10'
|
||||
default: return 'text-gray-400 border-gray-400'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
switch (status) {
|
||||
case 'primary': return 'Primary'
|
||||
case 'active': return 'Active Use'
|
||||
case 'occasional': return 'Occasional Use'
|
||||
default: return status
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<section 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-6 text-gray-200 flex items-center gap-2">
|
||||
<Bot size={24} />
|
||||
My AI Stack
|
||||
</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{tools.map((tool, index) => (
|
||||
<div key={index} className="p-4 border border-gray-700 rounded-lg hover:border-gray-500 transition-all duration-300 flex flex-col">
|
||||
<div className="flex items-start justify-between mb-3 flex-1">
|
||||
<div className="flex items-center gap-3">
|
||||
{tool.icon && <tool.icon className="text-2xl text-gray-300" />}
|
||||
{tool.svg && (
|
||||
<div className="w-6 h-6 text-gray-300 fill-current">
|
||||
{tool.svg}
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-200">{tool.name}</h3>
|
||||
<p className="text-sm text-gray-400">{tool.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-between mt-auto">
|
||||
<span className={`text-xs px-2 py-1 rounded-full border ${getStatusColor(tool.status)}`}>
|
||||
{getStatusLabel(tool.status)}
|
||||
</span>
|
||||
<span className="flex flex-row items-center gap-4">
|
||||
{tool.link && (
|
||||
<Link href={tool.link} className="text-blue-400 hover:text-blue-300 text-sm">
|
||||
View →
|
||||
</Link>
|
||||
)}
|
||||
{tool.usage && (
|
||||
<Link href={tool.usage} className="text-blue-400 hover:text-blue-300 text-sm">
|
||||
Usage →
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
import { Star } from 'lucide-react'
|
||||
import type { FavoriteModel } from '../types'
|
||||
|
||||
interface FavoriteModelsProps {
|
||||
models: FavoriteModel[]
|
||||
}
|
||||
|
||||
export default function FavoriteModels({ models }: FavoriteModelsProps) {
|
||||
return (
|
||||
<section 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-6 text-gray-200 flex items-center gap-2">
|
||||
<Star size={24} />
|
||||
Favorite Models
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{models.map((model, index) => (
|
||||
<div key={index} className="p-4 bg-gray-800/50 rounded-lg">
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<div>
|
||||
<h3 className="font-semibold text-gray-200">{model.name}</h3>
|
||||
<p className="text-sm text-gray-400">{model.provider}</p>
|
||||
</div>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={14}
|
||||
className={i < model.rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-600'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-gray-300">{model.review}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,54 +0,0 @@
|
|||
import { MessageSquare, Star } from 'lucide-react'
|
||||
import type { AIReview } from '../types'
|
||||
|
||||
interface ToolReviewsProps {
|
||||
reviews: AIReview[]
|
||||
}
|
||||
|
||||
export default function ToolReviews({ reviews }: ToolReviewsProps) {
|
||||
return (
|
||||
<section 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-6 text-gray-200 flex items-center gap-2">
|
||||
<MessageSquare size={24} />
|
||||
Tool Reviews
|
||||
</h2>
|
||||
<div className="space-y-4">
|
||||
{reviews.map((review, index) => (
|
||||
<div key={index} className="p-4 bg-gray-800/50 rounded-lg">
|
||||
<div className="flex justify-between items-center mb-3">
|
||||
<h3 className="font-semibold text-gray-200">{review.tool}</h3>
|
||||
<div className="flex gap-1">
|
||||
{[...Array(5)].map((_, i) => (
|
||||
<Star
|
||||
key={i}
|
||||
size={14}
|
||||
className={i < review.rating ? 'fill-yellow-400 text-yellow-400' : 'text-gray-600'}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2 mb-2 text-sm">
|
||||
<div>
|
||||
<p className="text-green-400 font-medium mb-1">Pros:</p>
|
||||
<ul className="text-gray-300 space-y-1">
|
||||
{review.pros.map((pro, i) => (
|
||||
<li key={i} className="text-xs">• {pro}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-red-400 font-medium mb-1">Cons:</p>
|
||||
<ul className="text-gray-300 space-y-1">
|
||||
{review.cons.map((con, i) => (
|
||||
<li key={i} className="text-xs">• {con}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-sm text-blue-400 font-medium">{review.verdict}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
import { Trophy, ChevronRight } from 'lucide-react'
|
||||
import { SiClaude } from 'react-icons/si'
|
||||
import Link from '@/components/objects/Link'
|
||||
|
||||
export default function TopPick() {
|
||||
return (
|
||||
<div className="px-4 mb-4">
|
||||
<h2 className="text-4xl font-semibold mb-6 text-gray-200 flex items-center gap-2">
|
||||
<Trophy size={32} />
|
||||
Top Pick of 2025
|
||||
</h2>
|
||||
<div className="p-6 sm:p-8 border-2 border-[#c15f3c] rounded-lg bg-orange-500/5">
|
||||
<div className="grid md:grid-cols-2 gap-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<SiClaude className="text-6xl text-[#c15f3c]" />
|
||||
<div>
|
||||
<h3 className="text-3xl font-bold text-gray-100">Claude</h3>
|
||||
<p className="text-gray-400">by Anthropic</p>
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Link href="/ai/claude" className="text-blue-400 hover:text-blue-300 flex items-center gap-1">
|
||||
View My Usage <ChevronRight size={16} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<p className="text-gray-300">
|
||||
Claude has become my go-to AI assistant for coding, writing, and learning very quickly.
|
||||
I believe their Max 5x ($100/mo) is the best value for budget-conscious consumers like myself.
|
||||
</p>
|
||||
<div className='flex flex-col items-center gap-y-6 sm:flex-row sm:justify-between'>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
<span className="px-2 py-1 bg-gray-700 rounded text-xs text-gray-300">Claude Code</span>
|
||||
<span className="px-2 py-1 bg-gray-700 rounded text-xs text-gray-300">Best Tool Calling</span>
|
||||
<span className="px-2 py-1 bg-gray-700 rounded text-xs text-gray-300">High Value in Max Plan</span>
|
||||
<span className="px-2 py-1 bg-gray-700 rounded text-xs text-gray-300">Quite Fast Interface</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-end">
|
||||
<span className="px-3 py-1 bg-[#c15f3c]/20 text-[#c15f3c] rounded-full text-sm font-medium">
|
||||
Top Overall Pick
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
125
app/ai/data.tsx
125
app/ai/data.tsx
|
@ -1,125 +0,0 @@
|
|||
import {
|
||||
SiClaude,
|
||||
SiGithubcopilot,
|
||||
SiGooglegemini
|
||||
} from 'react-icons/si'
|
||||
import type { AITool, FavoriteModel, AIReview } from './types'
|
||||
|
||||
export const aiTools: AITool[] = [
|
||||
{
|
||||
name: "Claude Max 5x",
|
||||
icon: SiClaude,
|
||||
description: "My favorite model provider for general use and coding",
|
||||
status: "primary",
|
||||
usage: "/ai/claude",
|
||||
link: "https://claude.ai/"
|
||||
},
|
||||
{
|
||||
name: "GitHub Copilot Pro",
|
||||
icon: SiGithubcopilot,
|
||||
description: "Random edits when I don't want to start a Claude session",
|
||||
status: "active",
|
||||
link: "https://github.com/features/copilot"
|
||||
},
|
||||
{
|
||||
name: "Gemini Pro",
|
||||
icon: SiGooglegemini,
|
||||
description: "Chatting, asking questions, and image generation",
|
||||
status: "occasional",
|
||||
link: "https://gemini.google.com/"
|
||||
},
|
||||
{
|
||||
name: "v0 Free",
|
||||
svg: <svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>v0</title><path d="M14.066 6.028v2.22h5.729q.075-.001.148.005l-5.853 5.752a2 2 0 0 1-.024-.309V8.247h-2.353v5.45c0 2.322 1.935 4.222 4.258 4.222h5.675v-2.22h-5.675q-.03 0-.059-.003l5.729-5.629q.006.082.006.166v5.465H24v-5.465a4.204 4.204 0 0 0-4.205-4.205zM0 8.245l8.28 9.266c.839.94 2.396.346 2.396-.914V8.245H8.19v5.44l-4.86-5.44Z"/></svg>,
|
||||
description: "Generating boilerplate UIs",
|
||||
status: "occasional",
|
||||
link: "https://v0.dev/"
|
||||
},
|
||||
{
|
||||
name: "Qwen",
|
||||
svg: (
|
||||
<svg viewBox="0 0 200 200" fill="none" xmlns="http://www.w3.org/2000/svg" className="size-8">
|
||||
<path d="M174.82 108.75L155.38 75L165.64 57.75C166.46 56.31 166.46 54.53 165.64 53.09L155.38 35.84C154.86 34.91 153.87 34.33 152.78 34.33H114.88L106.14 19.03C105.62 18.1 104.63 17.52 103.54 17.52H83.3C82.21 17.52 81.22 18.1 80.7 19.03L61.26 52.77H41.02C39.93 52.77 38.94 53.35 38.42 54.28L28.16 71.53C27.34 72.97 27.34 74.75 28.16 76.19L45.52 107.5L36.78 122.8C35.96 124.24 35.96 126.02 36.78 127.46L47.04 144.71C47.56 145.64 48.55 146.22 49.64 146.22H87.54L96.28 161.52C96.8 162.45 97.79 163.03 98.88 163.03H119.12C120.21 163.03 121.2 162.45 121.72 161.52L141.16 127.78H158.52C159.61 127.78 160.6 127.2 161.12 126.27L171.38 109.02C172.2 107.58 172.2 105.8 171.38 104.36L174.82 108.75Z" fill="url(#paint0_radial)"/>
|
||||
<path d="M119.12 163.03H98.88L87.54 144.71H49.64L61.26 126.39H80.7L38.42 55.29H61.26L83.3 19.03L93.56 37.35L83.3 55.29H161.58L151.32 72.54L170.76 106.28H151.32L141.16 88.34L101.18 163.03H119.12Z" fill="white"/>
|
||||
<path d="M127.86 79.83H76.14L101.18 122.11L127.86 79.83Z" fill="url(#paint1_radial)"/>
|
||||
<defs>
|
||||
<radialGradient id="paint0_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100 100) rotate(90) scale(100)">
|
||||
<stop stopColor="#665CEE"/>
|
||||
<stop offset="1" stopColor="#332E91"/>
|
||||
</radialGradient>
|
||||
<radialGradient id="paint1_radial" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(100 100) rotate(90) scale(100)">
|
||||
<stop stopColor="#665CEE"/>
|
||||
<stop offset="1" stopColor="#332E91"/>
|
||||
</radialGradient>
|
||||
</defs>
|
||||
</svg>
|
||||
),
|
||||
description: "My favorite open source LLM for chatting",
|
||||
status: "occasional",
|
||||
link: "https://chat.qwen.ai/"
|
||||
},
|
||||
]
|
||||
|
||||
export const favoriteModels: FavoriteModel[] = [
|
||||
{
|
||||
name: "Claude 4 Sonnet",
|
||||
provider: "Anthropic",
|
||||
review: "The perfect balance of capability, speed, and price. Perfect for development with React.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Claude 4.1 Opus",
|
||||
provider: "Anthropic",
|
||||
review: "Amazing planner, useful for Plan Mode in Claude Code. Useful in code generation too.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Qwen3-235B-A22B",
|
||||
provider: "Alibaba",
|
||||
review: "The OG thinking model. Amazing, funny, and smart for chats. Surprisingly good at coding too.",
|
||||
rating: 5
|
||||
},
|
||||
{
|
||||
name: "Gemini 2.5 Pro",
|
||||
provider: "Google",
|
||||
review: "Amazing for Deep Research and reasoning tasks. I hate it for coding.",
|
||||
rating: 4
|
||||
},
|
||||
{
|
||||
name: "gemma3 27B",
|
||||
provider: "Google",
|
||||
review: "My favorite for playing around with AI or creating a project. Easy to run locally and open weight!",
|
||||
rating: 4
|
||||
},
|
||||
]
|
||||
|
||||
export const aiReviews: AIReview[] = [
|
||||
{
|
||||
tool: "Claude Code",
|
||||
rating: 5,
|
||||
pros: ["Flagship models", "High usage limits", "Exceptional Claude integration"],
|
||||
cons: ["Can be slow", "High investment cost to get value"],
|
||||
verdict: "Best overall for Claude lovers"
|
||||
},
|
||||
{
|
||||
tool: "Cursor",
|
||||
rating: 4,
|
||||
pros: ["Works like magic", "Lots of model support", "Huge ecosystem and community"],
|
||||
cons: ["Expensive", "Hype around it is dying", "Unclear/manipulative pricing"],
|
||||
verdict: "Great all-rounder, slowly dying"
|
||||
},
|
||||
{
|
||||
tool: "Trae",
|
||||
rating: 4,
|
||||
pros: ["Good UI/UX", "Very budget-friendly", "Fantastic premium usage limits"],
|
||||
cons: ["No thinking", "Occasional parsing issues"],
|
||||
verdict: "Essential for productivity"
|
||||
},
|
||||
{
|
||||
tool: "GitHub Copilot",
|
||||
rating: 3,
|
||||
pros: ["Latest models", "Great autocomplete", "Budget-friendly subscription price"],
|
||||
cons: ["No thinking", "Low quality output", "Bad support for other IDEs"],
|
||||
verdict: "Good for casual use"
|
||||
},
|
||||
]
|
|
@ -1,40 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import { Brain } from 'lucide-react'
|
||||
import TopPick from './components/TopPick'
|
||||
import AIStack from './components/AIStack'
|
||||
import FavoriteModels from './components/FavoriteModels'
|
||||
import ToolReviews from './components/ToolReviews'
|
||||
import { aiTools, favoriteModels, aiReviews } from './data'
|
||||
|
||||
export default function AI() {
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="w-full px-2 sm:px-6">
|
||||
<div className="my-12 text-center">
|
||||
<div className="flex justify-center mb-6">
|
||||
<Brain size={60} />
|
||||
</div>
|
||||
<h1 className="text-4xl font-bold mb-2 text-gray-100 glow">AI</h1>
|
||||
<p className="text-gray-400">My journey with using LLMs</p>
|
||||
</div>
|
||||
|
||||
<TopPick />
|
||||
|
||||
<div className="p-4">
|
||||
<AIStack tools={aiTools} />
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 p-4">
|
||||
<FavoriteModels models={favoriteModels} />
|
||||
<ToolReviews reviews={aiReviews} />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
export interface AITool {
|
||||
name: string;
|
||||
icon?: React.ElementType;
|
||||
svg?: React.ReactNode;
|
||||
description: string;
|
||||
status: 'primary' | 'active' | 'occasional' | string;
|
||||
link?: string;
|
||||
usage?: string;
|
||||
}
|
||||
|
||||
export interface FavoriteModel {
|
||||
name: string;
|
||||
provider: string;
|
||||
review: string;
|
||||
rating: number;
|
||||
}
|
||||
|
||||
export interface AIReview {
|
||||
tool: string;
|
||||
rating: number;
|
||||
pros: string[];
|
||||
cons: string[];
|
||||
verdict: string;
|
||||
}
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import Button from '@/components/objects/Button'
|
||||
import ContactButton from '@/components/objects/ContactButton'
|
||||
import { Phone } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { SiGithub, SiForgejo, SiTelegram } from 'react-icons/si'
|
||||
import { Mail, Smartphone } from 'lucide-react'
|
||||
import { faPhone, faEnvelope } from '@fortawesome/free-solid-svg-icons'
|
||||
import { faGithub, faTelegram, faBluesky, faXTwitter } from '@fortawesome/free-brands-svg-icons'
|
||||
|
||||
export default function Contact() {
|
||||
const { t } = useTranslation();
|
||||
|
@ -23,65 +23,56 @@ export default function Contact() {
|
|||
];
|
||||
|
||||
const contactButtonLabels = [
|
||||
"ihatenodejs",
|
||||
"aidan",
|
||||
"p0ntu5",
|
||||
"+1 802-416-9516",
|
||||
"aidan@p0ntus.com",
|
||||
t('contact.buttons.github'),
|
||||
t('contact.buttons.telegram'),
|
||||
t('contact.buttons.bluesky'),
|
||||
t('contact.buttons.x'),
|
||||
t('contact.buttons.phone'),
|
||||
t('contact.buttons.email')
|
||||
];
|
||||
|
||||
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 = [
|
||||
<SiGithub key="github" />,
|
||||
<SiForgejo key="forgejo" />,
|
||||
<SiTelegram key="telegram" />,
|
||||
<Smartphone key="smartphone" />,
|
||||
<Mail key="mail" />
|
||||
];
|
||||
const contactButtonIcons = [faGithub, faTelegram, faBluesky, faXTwitter, faPhone, faEnvelope];
|
||||
|
||||
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="flex flex-col gap-4">
|
||||
<div className="flex justify-center">
|
||||
<Phone size={60} />
|
||||
</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('contact.title')}
|
||||
</h1>
|
||||
<div className='mb-6 flex justify-center'>
|
||||
<Phone size={60} />
|
||||
</div>
|
||||
<div className="flex flex-col gap-8 mt-8">
|
||||
<div className="flex flex-wrap justify-center gap-3">
|
||||
{contactButtonLabels.map((label, index) => (
|
||||
<Button
|
||||
key={index}
|
||||
href={contactButtonHrefs[index]}
|
||||
target="_blank"
|
||||
variant="rounded"
|
||||
icon={contactButtonIcons[index]}
|
||||
>
|
||||
{label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<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">{text}</p>
|
||||
))}
|
||||
</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)' }}>
|
||||
{t('contact.title')}
|
||||
</h1>
|
||||
<div className="p-6 space-y-4">
|
||||
{contactButtonLabels.map((label, index) => (
|
||||
<ContactButton
|
||||
key={index}
|
||||
label={label}
|
||||
href={contactButtonHrefs[index]}
|
||||
icon={contactButtonIcons[index]}
|
||||
className='mr-3'
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{sections.map((section, sectionIndex) => (
|
||||
<div key={sectionIndex}>
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200 mt-10">{section.title}</h2>
|
||||
{section.texts.map((text, index) => (
|
||||
<p key={index} className="text-gray-300 mb-4">{text}</p>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
|
|
|
@ -1,199 +0,0 @@
|
|||
import Header from "@/components/Header"
|
||||
import Footer from "@/components/Footer"
|
||||
import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
HardDrive,
|
||||
Hash,
|
||||
Music,
|
||||
} from "lucide-react"
|
||||
import { FaGoogle } from "react-icons/fa"
|
||||
import { VscTerminalLinux } from "react-icons/vsc"
|
||||
import { MdOutlineAndroid } from "react-icons/md"
|
||||
import { LuPackageOpen } from "react-icons/lu"
|
||||
import { RiTelegram2Fill } from "react-icons/ri"
|
||||
import Image from "next/image"
|
||||
import Link from "@/components/objects/Link"
|
||||
|
||||
export default function Bonito() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow px-6 py-12 md:py-16">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col lg:flex-row items-start gap-12 lg:gap-16">
|
||||
<div className="w-full lg:w-1/3 flex justify-center">
|
||||
<Image
|
||||
src="/img/bonito.png"
|
||||
alt="Google Pixel 3a XL (bonito)"
|
||||
width={450}
|
||||
height={450}
|
||||
className="w-full max-w-md h-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-2/3">
|
||||
<div className="text-center lg:text-left mb-12">
|
||||
<h1 className="text-4xl font-semibold mb-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<FaGoogle size={30} className="mr-2" />
|
||||
Pixel 3a XL
|
||||
</h1>
|
||||
<h3 className="text-xl font-semibold mb-8 text-slate-500">bonito</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 lg:gap-16">
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-2" />
|
||||
Specs
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-3" size={20} />
|
||||
<b className="mr-2">Chipset:</b> Qualcomm Snapdragon 670
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<HardDrive className="mr-3" size={20} />
|
||||
<b className="mr-2">Storage:</b> 64GB
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<MemoryStick className="mr-3" size={20} />
|
||||
<b className="mr-2">RAM:</b> 4GB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Hash className="mr-2" />
|
||||
Modifications
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<VscTerminalLinux className="mr-3" size={20} />
|
||||
<b className="mr-2">Kernel Version:</b>
|
||||
4.9.337
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<MdOutlineAndroid className="mr-3" size={20} />
|
||||
<b className="mr-2">ROM:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://www.ubuntu-touch.io"
|
||||
>
|
||||
Ubuntu Touch
|
||||
</Link>
|
||||
</p>
|
||||
{/*<p className="flex items-center justify-center lg:justify-start">
|
||||
<Hammer className="mr-3" size={20} />
|
||||
<b className="mr-2">Root:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/topjohnwu/Magisk"
|
||||
>
|
||||
Magisk
|
||||
</Link>
|
||||
N/A
|
||||
</p>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<LuPackageOpen className="mr-2" />
|
||||
Apps
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Music className="mr-3" size={20} />
|
||||
<b className="mr-2">Music:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/arubislander/uSonic"
|
||||
>
|
||||
uSonic
|
||||
</Link>
|
||||
</p>
|
||||
{/*<p className="flex items-center justify-center lg:justify-start">
|
||||
<Folder className="mr-3" size={20} />
|
||||
<b className="mr-2">Files:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://mixplorer.com/"
|
||||
>
|
||||
MiXplorer Beta
|
||||
</Link>
|
||||
N/A
|
||||
</p>*/}
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<RiTelegram2Fill className="mr-3" size={20} />
|
||||
<b className="mr-2">Telegram Client:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://open-store.io/app/teleports.ubports"
|
||||
>
|
||||
TELEports
|
||||
</Link>
|
||||
</p>
|
||||
{/*<p className="flex items-center justify-center lg:justify-start">
|
||||
<FaYoutube className="mr-3" size={20} />
|
||||
<b className="mr-2">YouTube:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/polymorphicshade/Tubular"
|
||||
>
|
||||
Tubular
|
||||
</Link>
|
||||
</p>*/}
|
||||
</div>
|
||||
</div>
|
||||
{/*<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Layers className="mr-2" />
|
||||
Modules
|
||||
</h1>
|
||||
<ul className="list-disc list-inside space-y-3">
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/bindhosts/bindhosts"
|
||||
>
|
||||
bindhosts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/Keinta15/Magisk-iOS-Emoji"
|
||||
>
|
||||
Magisk iOS Emoji
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>*/}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,249 +0,0 @@
|
|||
import Header from "@/components/Header"
|
||||
import Footer from "@/components/Footer"
|
||||
import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
HardDrive,
|
||||
Hash,
|
||||
Hammer,
|
||||
Music,
|
||||
Folder,
|
||||
Layers,
|
||||
SquarePen
|
||||
} from "lucide-react"
|
||||
import { FaGoogle, FaYoutube } from "react-icons/fa"
|
||||
import { VscTerminalLinux } from "react-icons/vsc"
|
||||
import { MdOutlineAndroid } from "react-icons/md"
|
||||
import { LuPackageOpen } from "react-icons/lu"
|
||||
import { RiTelegram2Fill } from "react-icons/ri"
|
||||
import Image from "next/image"
|
||||
import Link from "@/components/objects/Link"
|
||||
import { FaStarHalfStroke, FaStar } from "react-icons/fa6"
|
||||
|
||||
export default function Cheetah() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow px-6 py-12 md:py-16">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col lg:flex-row items-start gap-12 lg:gap-16">
|
||||
<div className="w-full lg:w-1/3 flex justify-center">
|
||||
<Image
|
||||
src="/img/cheetah.png"
|
||||
alt="Google Pixel 7 Pro (cheetah)"
|
||||
width={450}
|
||||
height={450}
|
||||
className="w-full max-w-md h-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-2/3">
|
||||
<div className="text-center lg:text-left mb-12">
|
||||
<h1 className="text-4xl font-semibold mb-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<FaGoogle size={30} className="mr-2" />
|
||||
Pixel 7 Pro
|
||||
</h1>
|
||||
<h3 className="text-xl font-semibold mb-8 text-slate-500">cheetah</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12 lg:gap-16">
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-2" />
|
||||
Specs
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-3" size={20} />
|
||||
<b className="mr-2">CPU:</b> Google Tensor G2
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<HardDrive className="mr-3" size={20} />
|
||||
<b className="mr-2">Storage:</b> 128GB
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<MemoryStick className="mr-3" size={20} />
|
||||
<b className="mr-2">RAM:</b> 12GB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Hash className="mr-2" />
|
||||
Modifications
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<VscTerminalLinux className="mr-3" size={20} />
|
||||
<b className="mr-2">Kernel:</b>
|
||||
6.1.99-android14
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<MdOutlineAndroid className="mr-3" size={20} />
|
||||
<b className="mr-2">ROM:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://crdroid.net"
|
||||
>
|
||||
crDroid Android 11.6
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Hammer className="mr-3" size={20} />
|
||||
<b className="mr-2">Root:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/rifsxd/KernelSU-Next"
|
||||
>
|
||||
KernelSU-Next
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<LuPackageOpen className="mr-2" />
|
||||
Apps
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Music className="mr-3" size={20} />
|
||||
<b className="mr-2">Music:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://tidal.com"
|
||||
>
|
||||
Tidal
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Folder className="mr-3" size={20} />
|
||||
<b className="mr-2">Files:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://mixplorer.com/"
|
||||
>
|
||||
MiXplorer
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<RiTelegram2Fill className="mr-3" size={20} />
|
||||
<b className="mr-2">TG Client:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://t.me/AyuGramReleases"
|
||||
>
|
||||
AyuGram
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<FaYoutube className="mr-3" size={20} />
|
||||
<b className="mr-2">YouTube:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://revanced.app"
|
||||
>
|
||||
ReVanced
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Layers className="mr-2" />
|
||||
Modules
|
||||
</h1>
|
||||
<ul className="list-disc list-inside space-y-3">
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/bindhosts/bindhosts"
|
||||
>
|
||||
bindhosts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/EmojiReplacer/Emoji-Replacer"
|
||||
>
|
||||
Emoji Replacer
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/PerformanC/ReZygisk"
|
||||
>
|
||||
ReZygisk
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/JingMatrix/LSPosed"
|
||||
>
|
||||
LSPosed JingMatrix
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<SquarePen className="mr-2" />
|
||||
Review
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<b className="mr-2">Rating:</b>
|
||||
<span className="flex items-center gap-1">
|
||||
<FaStar size={15} /> <FaStar size={15} /> <FaStar size={15} /> <FaStar size={15} /> <FaStarHalfStroke size={15} />
|
||||
</span>
|
||||
</p>
|
||||
<div className="space-y-4 text-sm lg:text-base">
|
||||
<p>
|
||||
Coming from a Galaxy A32 5G, the Pixel 7 Pro is a massive upgrade. The Tensor chip is highly performant, and with 12GB of RAM, the device is extremely snappy.
|
||||
</p>
|
||||
<p>
|
||||
I have had some issues with battery, although this may be due to Play Integrity Fix, which is known to consume battery. However, the camera has been a massive improvement, and the photos it is capable of taking are amazing.
|
||||
</p>
|
||||
<p>
|
||||
While the volume buttons did fall off, I do not discredit them for this, as Android makes it easy to have customizable on-screen volume buttons, something iPhones do not have.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,247 +0,0 @@
|
|||
import Header from "@/components/Header"
|
||||
import Footer from "@/components/Footer"
|
||||
import {
|
||||
Cpu,
|
||||
MemoryStick,
|
||||
HardDrive,
|
||||
Hash,
|
||||
Hammer,
|
||||
Music,
|
||||
Folder,
|
||||
Layers,
|
||||
} from "lucide-react"
|
||||
import { FaGoogle, FaYoutube } from "react-icons/fa"
|
||||
import { VscTerminalLinux } from "react-icons/vsc"
|
||||
import { MdOutlineAndroid } from "react-icons/md"
|
||||
import { LuPackageOpen } from "react-icons/lu"
|
||||
import { RiTelegram2Fill } from "react-icons/ri"
|
||||
import Image from "next/image"
|
||||
import Link from "@/components/objects/Link"
|
||||
|
||||
export default function Cheetah() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow px-6 py-12 md:py-16">
|
||||
<div className="max-w-7xl mx-auto">
|
||||
<div className="flex flex-col lg:flex-row items-start gap-12 lg:gap-16">
|
||||
<div className="w-full lg:w-1/3 flex justify-center">
|
||||
<Image
|
||||
src="/img/komodo.png"
|
||||
alt="Google Pixel 9 Pro XL (komodo)"
|
||||
width={450}
|
||||
height={450}
|
||||
className="w-full max-w-md h-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full lg:w-2/3">
|
||||
<div className="text-center lg:text-left mb-12">
|
||||
<h1 className="text-4xl font-semibold mb-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<FaGoogle size={30} className="mr-2" />
|
||||
Pixel 9 Pro XL
|
||||
</h1>
|
||||
<h3 className="text-xl font-semibold mb-8 text-slate-500">komodo</h3>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-12 lg:gap-16">
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-2" />
|
||||
Specs
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-3" size={20} />
|
||||
<b className="mr-2">CPU:</b> Google Tensor G4
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<HardDrive className="mr-3" size={20} />
|
||||
<b className="mr-2">Storage:</b> 128GB
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<MemoryStick className="mr-3" size={20} />
|
||||
<b className="mr-2">RAM:</b> 16GB
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Hash className="mr-2" />
|
||||
Modifications
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<VscTerminalLinux className="mr-3" size={20} />
|
||||
<b className="mr-2">Kernel:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/WildKernels/GKI_KernelSU_SUSFS"
|
||||
>
|
||||
6.1.138-android14-SUSFS-Wild
|
||||
</Link>
|
||||
</p>;
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<MdOutlineAndroid className="mr-3" size={20} />
|
||||
<b className="mr-2">ROM:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://developer.android.com/about/versions/16/qpr2"
|
||||
>
|
||||
Android 16 Beta QPR2
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Hammer className="mr-3" size={20} />
|
||||
<b className="mr-2">Root:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/rifsxd/KernelSU-Next"
|
||||
>
|
||||
KernelSU-Next
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-8">
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<LuPackageOpen className="mr-2" />
|
||||
Apps
|
||||
</h1>
|
||||
<div className="space-y-4">
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Music className="mr-3" size={20} />
|
||||
<b className="mr-2">Music:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://tidal.com"
|
||||
>
|
||||
Tidal
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<Folder className="mr-3" size={20} />
|
||||
<b className="mr-2">Files:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://mixplorer.com/"
|
||||
>
|
||||
MiXplorer
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<RiTelegram2Fill className="mr-3" size={20} />
|
||||
<b className="mr-2">TG Client:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://t.me/AyuGramReleases"
|
||||
>
|
||||
AyuGram
|
||||
</Link>
|
||||
</p>
|
||||
<p className="flex items-center justify-center lg:justify-start">
|
||||
<FaYoutube className="mr-3" size={20} />
|
||||
<b className="mr-2">YouTube:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://revanced.app"
|
||||
>
|
||||
ReVanced
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center lg:text-left">
|
||||
<h1 className="text-3xl font-semibold mb-6 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Layers className="mr-2" />
|
||||
Modules
|
||||
</h1>
|
||||
<ul className="list-disc list-inside space-y-3">
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://modules.lol/module/kowx712-bindhosts"
|
||||
>
|
||||
bindhosts
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/EmojiReplacer/Emoji-Replacer"
|
||||
>
|
||||
Emoji Replacer
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://modules.lol/module/entr0pia-f-droid-privileged-extension-installer"
|
||||
>
|
||||
F-Droid Privileged Extension
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://modules.lol/module/sidex15-susfs"
|
||||
>
|
||||
SUSFS-FOR-KERNELSU
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://modules.lol/module/5ec1cff-tricky-store"
|
||||
>
|
||||
Tricky Store
|
||||
</Link>
|
||||
</li>
|
||||
<li>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://modules.lol/module/dpejoh-and-yuri-yurikey"
|
||||
>
|
||||
Yuri Keybox Manager
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,7 +1,8 @@
|
|||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import { Link } from "lucide-react"
|
||||
import { TbCurrencyDollarOff } from "react-icons/tb";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"
|
||||
import { faBan } from "@fortawesome/free-solid-svg-icons"
|
||||
import domains from "@/public/data/domains.json"
|
||||
|
||||
export default function Domains() {
|
||||
|
@ -10,16 +11,17 @@ 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="flex flex-col gap-4">
|
||||
<div className="flex justify-center">
|
||||
<Link size={60} />
|
||||
</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)' }}>
|
||||
My Domains
|
||||
</h1>
|
||||
<div className="mb-6 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)" }}
|
||||
>
|
||||
My Domains
|
||||
</h1>
|
||||
<div className="mb-4 p-4 pt-8 flex flex-col items-center space-y-2">
|
||||
<TbCurrencyDollarOff size={26} className="text-red-500" />
|
||||
<FontAwesomeIcon icon={faBan} className="text-red-500 text-xl" />
|
||||
<span className="text-red-500 font-medium text-center mt-1 mb-0">
|
||||
These domains are not for sale.
|
||||
</span>
|
||||
|
|
117
app/layout.tsx
117
app/layout.tsx
|
@ -1,67 +1,74 @@
|
|||
import React from 'react'
|
||||
import { Metadata } from 'next'
|
||||
import Head from 'next/head'
|
||||
import './globals.css'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import AnimatedTitle from '../components/AnimatedTitle'
|
||||
import I18nProvider from '../components/I18nProvider'
|
||||
"use client"
|
||||
|
||||
export const metadata: Metadata = {
|
||||
description: "The Internet home of Aidan. Come on in!",
|
||||
openGraph: {
|
||||
type: "website",
|
||||
url: "https://aidxn.cc",
|
||||
title: "aidxn.cc",
|
||||
description: "The Internet home of Aidan. Come on in!",
|
||||
siteName: "aidxn.cc",
|
||||
images: [
|
||||
{
|
||||
url: "https://aidxn.cc/android-icon-192x192.png",
|
||||
width: 192,
|
||||
height: 192,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
import React, { useEffect } from 'react'
|
||||
import './globals.css'
|
||||
import '@fortawesome/fontawesome-svg-core/styles.css'
|
||||
import { config } from '@fortawesome/fontawesome-svg-core'
|
||||
import { GeistSans } from 'geist/font/sans'
|
||||
import '../i18n'
|
||||
|
||||
config.autoAddCss = false
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
useEffect(() => {
|
||||
const title = 'aidxn.cc';
|
||||
let index = 1;
|
||||
let forward = true;
|
||||
const interval = setInterval(() => {
|
||||
document.title = title.substring(0, index);
|
||||
if (forward) {
|
||||
index++;
|
||||
if (index > title.length) {
|
||||
forward = false;
|
||||
index = title.length - 1;
|
||||
}
|
||||
} else {
|
||||
index--;
|
||||
if (index < 1) {
|
||||
forward = true;
|
||||
index = 1;
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<html lang="en" className="dark">
|
||||
<Head>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="language" content="English" />
|
||||
<meta name="author" content="aidxn.cc" />
|
||||
</Head>
|
||||
<body className={`${GeistSans.className} bg-gray-900 text-gray-100`}>
|
||||
<AnimatedTitle />
|
||||
<I18nProvider>
|
||||
<html lang="en" className="dark">
|
||||
<head>
|
||||
<link rel="apple-touch-icon" sizes="57x57" href="/apple-icon-57x57.png" />
|
||||
<link rel="apple-touch-icon" sizes="60x60" href="/apple-icon-60x60.png" />
|
||||
<link rel="apple-touch-icon" sizes="72x72" href="/apple-icon-72x72.png" />
|
||||
<link rel="apple-touch-icon" sizes="76x76" href="/apple-icon-76x76.png" />
|
||||
<link rel="apple-touch-icon" sizes="114x114" href="/apple-icon-114x114.png" />
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="/apple-icon-120x120.png" />
|
||||
<link rel="apple-touch-icon" sizes="144x144" href="/apple-icon-144x144.png" />
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="/apple-icon-152x152.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-icon-180x180.png" />
|
||||
<link rel="icon" type="image/png" sizes="192x192" href="/android-icon-192x192.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="96x96" href="/favicon-96x96.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
<meta name="msapplication-TileColor" content="#ffffff" />
|
||||
<meta name="msapplication-TileImage" content="/ms-icon-144x144.png" />
|
||||
<meta name="theme-color" content="#ffffff" />
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="description" content="The Internet home of Aidan. Come on in!" />
|
||||
<meta name="keywords" content="blog, android, developer" />
|
||||
<meta name="robots" content="index, follow" />
|
||||
<meta name="language" content="English" />
|
||||
<meta name="author" content="aidxn.cc" />
|
||||
</head>
|
||||
<body className={`${GeistSans.className} bg-gray-900 text-gray-100`}>
|
||||
{children}
|
||||
</I18nProvider>
|
||||
</body>
|
||||
</html>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -8,16 +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="flex flex-col gap-4">
|
||||
<div className="flex justify-center">
|
||||
<BookOpen size={60} />
|
||||
</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)' }}>
|
||||
Internet Manifesto
|
||||
</h1>
|
||||
<div className='mb-6 flex justify-center'>
|
||||
<BookOpen size={60} />
|
||||
</div>
|
||||
<div className="px-6 pt-12">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
|
||||
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
|
||||
Internet Manifesto
|
||||
</h1>
|
||||
<div className="px-6 pt-6">
|
||||
<h2 className="text-2xl font-semibold mb-4 text-gray-200">
|
||||
1. Empathy and Understanding
|
||||
</h2>
|
||||
<p className="text-gray-300 mb-4">
|
||||
|
@ -28,7 +26,6 @@ export default function Manifesto() {
|
|||
<li>Suspend judgment and seek to understand</li>
|
||||
<li>Recognize the humanity in every digital interaction</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||
2. Unconditional Sharing!
|
||||
</h2>
|
||||
|
@ -41,14 +38,12 @@ export default function Manifesto() {
|
|||
<li>Support open-source principles</li>
|
||||
<li>Create extensive documentation on all of my projects</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||
3. Genuine Human Connection
|
||||
</h2>
|
||||
<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.
|
||||
</p>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||
4. Privacy & Self-Hosted Services
|
||||
</h2>
|
||||
|
@ -65,7 +60,6 @@ export default function Manifesto() {
|
|||
<li>Focus my services on being free and open</li>
|
||||
<li>Suggest and support privacy-focused software</li>
|
||||
</ul>
|
||||
|
||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">
|
||||
I Commit
|
||||
</h2>
|
||||
|
|
26
app/music/page.tsx
Normal file
26
app/music/page.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
import Header from '@/components/Header'
|
||||
import MusicWidget from '@/components/widgets/Music'
|
||||
import MusicInfo from '@/components/objects/MusicInfo'
|
||||
import Footer from '@/components/Footer'
|
||||
import { Music as MusicNote } from "lucide-react";
|
||||
|
||||
export default function Music() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-12">
|
||||
<div className='mb-6 flex justify-center'>
|
||||
<MusicNote 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)' }}>
|
||||
Music and Me
|
||||
</h1>
|
||||
<div className="flex justify-center max-w-2xl gap-16 mx-auto pt-8">
|
||||
<MusicWidget />
|
||||
<MusicInfo />
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
149
app/page.tsx
149
app/page.tsx
|
@ -3,27 +3,13 @@
|
|||
import Header from '@/components/Header'
|
||||
import Footer from '@/components/Footer'
|
||||
import Button from '@/components/objects/Button'
|
||||
import Link from '@/components/objects/Link'
|
||||
import LastPlayed from '@/components/widgets/NowPlaying'
|
||||
|
||||
import Image from 'next/image'
|
||||
|
||||
import {CreditCard, Mail, PillBottle, Scale, UserCircle} from 'lucide-react'
|
||||
import { BsArrowClockwise } from "react-icons/bs";
|
||||
import { CreditCard, Mail, PillBottle, Scale } from 'lucide-react'
|
||||
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 {TbHeartHandshake, TbUserHeart, TbMessage} from "react-icons/tb";
|
||||
import {BiDonateHeart} from "react-icons/bi";
|
||||
import { SiGithubsponsors } from 'react-icons/si'
|
||||
|
||||
export default function Home() {
|
||||
const { t } = useTranslation()
|
||||
|
@ -57,125 +43,80 @@ 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="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 className="p-8 border-2 border-gray-700 rounded-lg hover:border-gray-600 transition-colors duration-300">
|
||||
<LastPlayed />
|
||||
</div>
|
||||
|
||||
{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">
|
||||
<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>
|
||||
)}
|
||||
<section key={secIndex} className="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>
|
||||
{mainStrings[secIndex].map((text: string, index: number) => (
|
||||
<p key={index} className="text-gray-300 leading-relaxed mt-2">
|
||||
{text}
|
||||
{secIndex === 2 && index === 2 && (
|
||||
<>
|
||||
{' '}
|
||||
<Link href="https://nvd.nist.gov/vuln/detail/CVE-2025-29927">
|
||||
CVE-2025-29927
|
||||
</Link>
|
||||
.
|
||||
</>
|
||||
)}
|
||||
</p>
|
||||
))}
|
||||
</section>
|
||||
))}
|
||||
|
||||
<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="flex flex-row items-center gap-2 text-2xl font-semibold mb-4 text-gray-200">
|
||||
<TbMessage />
|
||||
{t('home.contact.title')}
|
||||
</h2>
|
||||
<section id="contact" className="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>
|
||||
<p className="text-gray-300 mb-6">{t('home.contact.description')}</p>
|
||||
<Button
|
||||
href={'/contact'}
|
||||
icon={<Mail size={16} />}
|
||||
>
|
||||
{t('home.contact.button')}
|
||||
</Button>
|
||||
label={t('home.contact.button')}
|
||||
icon={Mail}
|
||||
/>
|
||||
</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">
|
||||
<h2 className="flex flex-row items-center gap-2 text-2xl font-semibold mb-4 text-gray-200">
|
||||
<BiDonateHeart />
|
||||
{t('home.donation.title')}
|
||||
</h2>
|
||||
<section id="donation" className="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>
|
||||
<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>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 md:text-sm gap-3">
|
||||
<Button
|
||||
href="https://unsilenced.org"
|
||||
icon={<FaHandcuffs />}
|
||||
href={'https://unsilenced.org'}
|
||||
label={t('home.donation.charities.unsilenced')}
|
||||
icon={FaHandcuffs}
|
||||
target="_blank"
|
||||
>
|
||||
{t('home.donation.charities.unsilenced')}
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
href="https://drugpolicy.org"
|
||||
icon={<PillBottle size={16} />}
|
||||
href={'https://drugpolicy.org'}
|
||||
label={t('home.donation.charities.drugpolicy')}
|
||||
icon={PillBottle}
|
||||
target="_blank"
|
||||
>
|
||||
{t('home.donation.charities.drugpolicy')}
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
href="https://www.aclu.org"
|
||||
icon={<Scale size={16} />}
|
||||
href={'https://www.aclu.org'}
|
||||
label={t('home.donation.charities.aclu')}
|
||||
icon={Scale}
|
||||
target="_blank"
|
||||
>
|
||||
{t('home.donation.charities.aclu')}
|
||||
</Button>
|
||||
<Button
|
||||
href="https://www.epicrestartfoundation.org"
|
||||
icon={<BsArrowClockwise size={16} />}
|
||||
target="_blank"
|
||||
>
|
||||
{t('home.donation.charities.epic-restart')}
|
||||
</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"
|
||||
icon={<CreditCard size={16} />}
|
||||
href={'https://donate.stripe.com/6oEeWVcXs9L9ctW4gj'}
|
||||
label={t('home.donation.donate.stripe')}
|
||||
icon={CreditCard}
|
||||
target="_blank"
|
||||
>
|
||||
{t('home.donation.donate.stripe')}
|
||||
</Button>
|
||||
/>
|
||||
<Button
|
||||
href="https://github.com/sponsors/ihatenodejs"
|
||||
icon={<SiGithubsponsors size={16} />}
|
||||
href={'https://github.com/sponsors/ihatenodejs'}
|
||||
label={t('home.donation.donate.github')}
|
||||
icon={SiGithubsponsors}
|
||||
target="_blank"
|
||||
>
|
||||
{t('home.donation.donate.github')}
|
||||
</Button>
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
@ -183,4 +124,4 @@ export default function Home() {
|
|||
<Footer />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
257
app/phone/page.tsx
Normal file
257
app/phone/page.tsx
Normal file
|
@ -0,0 +1,257 @@
|
|||
import Header from "@/components/Header"
|
||||
import Footer from "@/components/Footer"
|
||||
import { Smartphone, Cpu, MemoryStick, HardDrive, Hash, Hammer, Music, Folder, Layers, SquarePen } from "lucide-react"
|
||||
import { FaGoogle, FaYoutube } from "react-icons/fa"
|
||||
import { VscTerminalLinux } from "react-icons/vsc"
|
||||
import { MdOutlineAndroid } from "react-icons/md"
|
||||
import { LuPackageOpen } from "react-icons/lu"
|
||||
import { RiTelegram2Fill } from "react-icons/ri"
|
||||
import Image from "next/image"
|
||||
import Link from "@/components/objects/Link"
|
||||
import { FaStarHalfStroke, FaStar } from "react-icons/fa6"
|
||||
|
||||
export default function About() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-8 md:py-12">
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<div className="mb-6 flex justify-center">
|
||||
<Smartphone 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)" }}
|
||||
>
|
||||
My Phone
|
||||
</h1>
|
||||
</div>
|
||||
<div className="px-6 pt-6">
|
||||
<div className="flex flex-col lg:flex-row items-start gap-6 md:gap-8">
|
||||
<div className="w-full max-w-sm mx-auto justify-start lg:justify-center lg:mx-0">
|
||||
<Image
|
||||
src="/img/cheetah.png"
|
||||
alt="Google Pixel 7 Pro (cheetah)"
|
||||
width={450}
|
||||
height={450}
|
||||
className="w-full h-auto"
|
||||
/>
|
||||
</div>
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 md:gap-8">
|
||||
<div className="w-full text-center lg:text-left">
|
||||
<h1 className="text-4xl font-semibold mt-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<FaGoogle size={30} className="mr-2" />
|
||||
Pixel 7 Pro
|
||||
</h1>
|
||||
<h3 className="text-xl font-semibold mb-4 text-slate-500">cheetah</h3>
|
||||
<hr className="mb-6 lg:mb-0"/>
|
||||
<h1 className="text-3xl font-semibold my-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-2" />
|
||||
Specifications
|
||||
</h1>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<Cpu className="mr-2" size={20} />
|
||||
<b className="mr-1">CPU:</b> Google Tensor G2
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<HardDrive className="mr-2" size={20} />
|
||||
<b className="mr-1">Storage:</b> 128GB
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<MemoryStick className="mr-2" size={20} />
|
||||
<b className="mr-1">RAM:</b> 12GB
|
||||
</p>
|
||||
<hr className="my-6 lg:mt-4 lg:mb-0" />
|
||||
<h1 className="text-3xl font-semibold my-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Hash className="mr-2" />
|
||||
Modifications
|
||||
</h1>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<VscTerminalLinux className="mr-2" size={20} />
|
||||
<b className="mr-1">Kernel:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/WildPlusKernel/GKI_KernelSU_SUSFS/"
|
||||
>
|
||||
android13-5.10.WILD
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<MdOutlineAndroid className="mr-2" size={20} />
|
||||
<b className="mr-1">ROM:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://axionaosp.github.io"
|
||||
>
|
||||
AxionAOSP v1.1
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<Hammer className="mr-2" size={20} />
|
||||
<b className="mr-1">Root:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/rifsxd/KernelSU-Next"
|
||||
>
|
||||
KernelSU-Next
|
||||
</Link>
|
||||
</p>
|
||||
</div>
|
||||
<hr className="lg:hidden" />
|
||||
<div className="w-full text-center lg:text-left lg:ml-8">
|
||||
<h1 className="text-3xl font-semibold mb-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<LuPackageOpen className="mr-2" />
|
||||
Apps
|
||||
</h1>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<Music className="mr-2" size={20} />
|
||||
<b className="mr-1">Music:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://qobuz.com"
|
||||
>
|
||||
Qobuz
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<Folder className="mr-2" size={20} />
|
||||
<b className="mr-1">Files:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://mixplorer.com/"
|
||||
>
|
||||
MiXplorer Beta
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<RiTelegram2Fill className="mr-2" size={20} />
|
||||
<b className="mr-1">Telegram:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/arslan4k1390/Cherrygram"
|
||||
>
|
||||
Cherrygram
|
||||
</Link>
|
||||
</p>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<FaYoutube className="mr-2" size={20} />
|
||||
<b className="mr-1">YouTube:</b>
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/NoName-exe/revanced-extended"
|
||||
>
|
||||
ReVanced Extended
|
||||
</Link>
|
||||
</p>
|
||||
<hr className="mt-8 mb-6 lg:my-4" />
|
||||
<h1 className="text-3xl font-semibold mb-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<Layers className="mr-2" />
|
||||
Modules
|
||||
</h1>
|
||||
<ul className="list-disc list-inside space-y-1">
|
||||
<li className="mb-0.5">
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/chiteroman/PlayIntegrityFix"
|
||||
>
|
||||
Play Integrity Fix
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-0.5">
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/5ec1cff/TrickyStore"
|
||||
>
|
||||
Tricky Store
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-0.5">
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/LSPosed/LSPosed.github.io/releases"
|
||||
>
|
||||
Shamiko
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-0.5">
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/mywalkb/LSPosed_mod/releases"
|
||||
>
|
||||
LSPosed_mod
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-0.5">
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/Dr-TSNG/ZygiskNext"
|
||||
>
|
||||
Zygisk Next
|
||||
</Link>
|
||||
</li>
|
||||
<li className="mb-0.5">
|
||||
<Link
|
||||
className="underline hover:glow transition-all"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://github.com/sidex15/susfs4ksu-module"
|
||||
>
|
||||
SUSFS for KernelSU
|
||||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<hr className="mt-2 lg:mt-0 lg:hidden" />
|
||||
<div className="w-full lg:mt-0 text-center lg:text-left lg:ml-8">
|
||||
<h1 className="text-3xl font-semibold mb-3 text-gray-200 flex items-center justify-center lg:justify-start">
|
||||
<SquarePen className="mr-2" />
|
||||
Review
|
||||
</h1>
|
||||
<p className="mb-1 flex items-center justify-center lg:justify-start">
|
||||
<b className="mr-1">Rating:</b>
|
||||
<FaStar size={15} /> <FaStar size={15} /> <FaStar size={15} /> <FaStar size={15} />{" "}
|
||||
<FaStarHalfStroke size={15} />
|
||||
</p>
|
||||
<p className="max-w-sm mt-4 lg:text-sm">
|
||||
Coming from a Galaxy A32 5G, the Pixel 7 Pro is a massive upgrade. The Tensor chip is highly performant, and with 12GB of RAM, the device is extremely snappy.
|
||||
</p>
|
||||
<p className="max-w-sm mt-4 lg:text-sm">
|
||||
I have had some issues with battery, although this may be due to Play Integrity Fix, which is known to consume battery. However, the camera has been a massive improvement, and the photos it is capable of taking are amazing.
|
||||
</p>
|
||||
<p className="max-w-sm mt-4 lg:text-sm">
|
||||
While the volume buttons did fall off, I do not discredit them for this, as Android makes it easy to have customizable on-screen volume buttons, something iPhones do not have.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export const robots: MetadataRoute.Robots = {
|
||||
rules: {
|
||||
userAgent: '*',
|
||||
allow: '/',
|
||||
},
|
||||
sitemap: 'https://aidxn.cc/sitemap.xml',
|
||||
}
|
||||
|
||||
export default function handler(): MetadataRoute.Robots {
|
||||
return robots
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
import type { MetadataRoute } from 'next'
|
||||
|
||||
export default function sitemap(): MetadataRoute.Sitemap {
|
||||
return [
|
||||
{
|
||||
url: 'https://aidxn.cc',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 1.0,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/about',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/ai',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/ai/claude',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'daily',
|
||||
priority: 0.9,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/contact',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/domains',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'monthly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/device/cheetah',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly' /* yes, i really re-flash roms this often */,
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/device/bonito',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/device/komodo',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'weekly',
|
||||
priority: 0.8,
|
||||
},
|
||||
{
|
||||
url: 'https://aidxn.cc/manifesto',
|
||||
lastModified: new Date(),
|
||||
changeFrequency: 'yearly',
|
||||
priority: 0.7,
|
||||
},
|
||||
]
|
||||
}
|
15
app/time-periods/early-summer-2024/page.tsx
Normal file
15
app/time-periods/early-summer-2024/page.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Header from '@/components/Header'
|
||||
import WhatWasGoingOn from '@/components/pages/time-periods/early-summer-2024/WhatWasGoingOn'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export default function EarlySummer2024() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-12">
|
||||
<WhatWasGoingOn />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
import Header from '@/components/Header'
|
||||
import WhatWasGoingOn from '@/components/pages/time-periods/early-summer-2024/WhatWasGoingOn'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export default function Music() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-12">
|
||||
<WhatWasGoingOn />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
15
app/time-periods/late-summer-2024/page.tsx
Normal file
15
app/time-periods/late-summer-2024/page.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Header from '@/components/Header'
|
||||
import WhatWasGoingOn from '@/components/pages/time-periods/late-summer-2024/WhatWasGoingOn'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export default function LateSummer2024() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-12">
|
||||
<WhatWasGoingOn />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
15
app/time-periods/late-summer-2024/what-was-going-on/page.tsx
Normal file
15
app/time-periods/late-summer-2024/what-was-going-on/page.tsx
Normal file
|
@ -0,0 +1,15 @@
|
|||
import Header from '@/components/Header'
|
||||
import WhatWasGoingOn from '@/components/pages/time-periods/late-summer-2024/WhatWasGoingOn'
|
||||
import Footer from '@/components/Footer'
|
||||
|
||||
export default function Music() {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
<Header />
|
||||
<main className="grow container mx-auto px-4 py-12">
|
||||
<WhatWasGoingOn />
|
||||
</main>
|
||||
<Footer />
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function AnimatedTitle() {
|
||||
useEffect(() => {
|
||||
const title = 'aidxn.cc';
|
||||
let index = 1;
|
||||
let forward = true;
|
||||
const interval = setInterval(() => {
|
||||
document.title = title.substring(0, index);
|
||||
if (forward) {
|
||||
index++;
|
||||
if (index > title.length) {
|
||||
forward = false;
|
||||
index = title.length - 1;
|
||||
}
|
||||
} else {
|
||||
index--;
|
||||
if (index < 1) {
|
||||
forward = true;
|
||||
index = 1;
|
||||
}
|
||||
}
|
||||
}, 500);
|
||||
return () => clearInterval(interval);
|
||||
}, []);
|
||||
return null;
|
||||
}
|
|
@ -2,21 +2,7 @@
|
|||
|
||||
import React, { useState, useRef, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import {
|
||||
House,
|
||||
Link as LinkIcon,
|
||||
User,
|
||||
Phone,
|
||||
BookOpen,
|
||||
X,
|
||||
Menu,
|
||||
Globe,
|
||||
ChevronDown,
|
||||
ChevronRight,
|
||||
Brain,
|
||||
Smartphone
|
||||
} from 'lucide-react'
|
||||
import { SiClaude, SiGoogle } from 'react-icons/si'
|
||||
import { House, Link as LinkIcon, User, Phone, BookOpen, Music, Rss, X, Menu, Globe, ChevronDown } from 'lucide-react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface NavItemProps {
|
||||
|
@ -34,187 +20,12 @@ const NavItem = ({ href, icon, children }: NavItemProps) => (
|
|||
</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 { i18n } = useTranslation();
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
|
||||
const languages = [
|
||||
{ code: 'en-US', name: 'English' },
|
||||
];
|
||||
|
@ -223,7 +34,7 @@ const LanguageSelector = () => {
|
|||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth < 1024);
|
||||
};
|
||||
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
|
@ -241,33 +52,9 @@ const LanguageSelector = () => {
|
|||
}
|
||||
};
|
||||
|
||||
if (isMobile) {
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}
|
||||
}, [isMobile]);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isMobile) {
|
||||
setIsOpen(true);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = (e: React.MouseEvent) => {
|
||||
if (!isMobile) {
|
||||
const relatedTarget = e.relatedTarget as HTMLElement;
|
||||
if (relatedTarget && dropdownRef.current?.contains(relatedTarget)) {
|
||||
return;
|
||||
}
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (isMobile) {
|
||||
setIsOpen(!isOpen);
|
||||
}
|
||||
};
|
||||
document.addEventListener('mousedown', handleClickOutside);
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||
}, []);
|
||||
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
@ -278,56 +65,53 @@ 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 (
|
||||
<div
|
||||
className="relative"
|
||||
ref={dropdownRef}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<button
|
||||
onClick={handleClick}
|
||||
<div className="relative" ref={dropdownRef}>
|
||||
<button
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
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' : ''}`}
|
||||
aria-expanded={isOpen}
|
||||
aria-haspopup="true"
|
||||
>
|
||||
<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} />
|
||||
{buttonContent}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<>
|
||||
{/* Invisible bridge to handle gap */}
|
||||
{!isMobile && (
|
||||
<div className="absolute right-0 top-full w-56 h-2 z-50" />
|
||||
)}
|
||||
<div
|
||||
className={`${
|
||||
isMobile
|
||||
? 'relative mt-2 w-full bg-gray-700/50 rounded-md'
|
||||
: 'absolute right-0 mt-2 w-56 bg-gray-800 rounded-lg shadow-xl border border-gray-700 z-50'
|
||||
}`}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="language-menu"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`block w-full text-left px-5 py-3 text-base rounded-md ${
|
||||
i18n.language === lang.code
|
||||
? 'text-white bg-gray-700'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
||||
} transition-all duration-300`}
|
||||
role="menuitem"
|
||||
>
|
||||
{lang.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
<div
|
||||
className={`${
|
||||
isMobile
|
||||
? 'relative mt-1 w-full bg-gray-800 rounded-md shadow-lg'
|
||||
: 'absolute right-0 mt-2 w-48 bg-gray-800 rounded-md shadow-lg z-50'
|
||||
}`}
|
||||
role="menu"
|
||||
aria-orientation="vertical"
|
||||
aria-labelledby="language-menu"
|
||||
>
|
||||
{languages.map((lang) => (
|
||||
<button
|
||||
key={lang.code}
|
||||
onClick={() => changeLanguage(lang.code)}
|
||||
className={`block w-full text-left px-4 py-2 text-sm ${
|
||||
i18n.language === lang.code
|
||||
? 'text-white bg-gray-700'
|
||||
: 'text-gray-300 hover:text-white hover:bg-gray-700'
|
||||
}`}
|
||||
role="menuitem"
|
||||
>
|
||||
{lang.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
@ -335,125 +119,35 @@ const LanguageSelector = () => {
|
|||
|
||||
export default function Header() {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [activeDropdown, setActiveDropdown] = useState<string | null>(null);
|
||||
|
||||
const toggleMenu = () => setIsOpen(!isOpen);
|
||||
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
className={`fixed inset-0 z-30 pointer-events-none transition-all duration-300 ${
|
||||
activeDropdown && !isMobile
|
||||
? 'backdrop-blur-sm opacity-100'
|
||||
: 'backdrop-blur-none opacity-0'
|
||||
}`}
|
||||
/>
|
||||
<header className="bg-gray-800 relative">
|
||||
{isOpen && (
|
||||
<div
|
||||
className="fixed inset-0 backdrop-blur-md z-40 lg:hidden"
|
||||
onClick={toggleMenu}
|
||||
/>
|
||||
)}
|
||||
<nav className="container mx-auto px-4 py-4 flex justify-between items-center relative z-50">
|
||||
<Link href="/" className="text-gray-300 hover:text-white text-2xl font-bold transition-all duration-300 hover:glow">
|
||||
aidxn.cc
|
||||
</Link>
|
||||
<button onClick={toggleMenu} className="lg:hidden text-gray-300 focus:outline-hidden">
|
||||
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
|
||||
</button>
|
||||
<ul className={`flex flex-col lg:flex-row space-y-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>
|
||||
<DropdownNavItem
|
||||
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="/domains" icon={LinkIcon}>Domains</NavItem>
|
||||
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
|
||||
<div className="lg:hidden">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</ul>
|
||||
<div className="hidden lg:block">
|
||||
<header className="bg-gray-800 shadow-lg">
|
||||
<nav className="container mx-auto px-4 py-4 flex justify-between items-center">
|
||||
<Link href="/" className="text-gray-300 hover:text-white text-2xl font-bold transition-all duration-300 hover:glow">
|
||||
aidxn.cc
|
||||
</Link>
|
||||
<button onClick={toggleMenu} className="lg:hidden text-gray-300 focus:outline-hidden">
|
||||
{isOpen ? <X className="text-2xl" /> : <Menu className="text-2xl" />}
|
||||
</button>
|
||||
<ul className={`flex flex-col lg:flex-row space-y-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-16 lg:top-auto p-4 lg:p-0 transition-all duration-300 ease-in-out ${isOpen ? 'flex' : 'hidden lg:flex'}`}>
|
||||
<NavItem href="/" icon={House}>Home</NavItem>
|
||||
<NavItem href="/about" icon={User}>About</NavItem>
|
||||
<NavItem href="/contact" icon={Phone}>Contact</NavItem>
|
||||
<NavItem href="/domains" icon={LinkIcon}>Domains</NavItem>
|
||||
<NavItem href="/manifesto" icon={BookOpen}>Manifesto</NavItem>
|
||||
<NavItem href="/music" icon={Music}>Music</NavItem>
|
||||
<NavItem href="https://disfunction.blog" icon={Rss}>Blog</NavItem>
|
||||
<div className="lg:hidden">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
</>
|
||||
</ul>
|
||||
<div className="hidden lg:block">
|
||||
<LanguageSelector />
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { ReactNode } from "react";
|
||||
import "../i18n";
|
||||
|
||||
export default function I18nProvider({ children }: { children: ReactNode }) {
|
||||
return <>{children}</>;
|
||||
}
|
24
components/objects/BackButton.tsx
Normal file
24
components/objects/BackButton.tsx
Normal file
|
@ -0,0 +1,24 @@
|
|||
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;
|
|
@ -1,40 +1,30 @@
|
|||
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, target, variant, className, icon, children }) => {
|
||||
if (!variant || variant === "primary") {
|
||||
return (
|
||||
<Link
|
||||
href={href}
|
||||
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}
|
||||
{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>
|
||||
)
|
||||
}
|
||||
const Button: React.FC<ButtonProps> = ({ href, label, icon, target, className }) => {
|
||||
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"
|
||||
)}
|
||||
target={target}
|
||||
>
|
||||
{icon && React.createElement(icon, { size: 20, className: "mr-2" })}
|
||||
{label}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
export default Button
|
26
components/objects/ContactButton.tsx
Normal file
26
components/objects/ContactButton.tsx
Normal file
|
@ -0,0 +1,26 @@
|
|||
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;
|
31
components/objects/MusicInfo.tsx
Normal file
31
components/objects/MusicInfo.tsx
Normal file
|
@ -0,0 +1,31 @@
|
|||
import React from 'react';
|
||||
import Button from './Button';
|
||||
|
||||
interface TimePeriod {
|
||||
title: string;
|
||||
slug: string;
|
||||
}
|
||||
|
||||
const timePeriods: TimePeriod[] = [
|
||||
{ title: 'Late Summer 2024', slug: 'late-summer-2024' },
|
||||
{ title: 'Early Summer 2024', slug: 'early-summer-2024' },
|
||||
];
|
||||
|
||||
const MusicInfo: React.FC = () => {
|
||||
return (
|
||||
<div>
|
||||
{timePeriods.map((period) => (
|
||||
<section key={period.slug} className="mb-12">
|
||||
<h2 className="text-2xl font-semibold mb-4">{period.title}</h2>
|
||||
<Button
|
||||
href={`/time-periods/${period.slug}/what-was-going-on`}
|
||||
label="WHAT WAS GOING ON"
|
||||
/>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default MusicInfo;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
"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
|
||||
|
|
@ -1,16 +1,14 @@
|
|||
"use client"
|
||||
|
||||
import {
|
||||
SiNextdotjs,
|
||||
SiLucide,
|
||||
SiVercel,
|
||||
SiCloudflarepages,
|
||||
SiSimpleicons,
|
||||
SiFontawesome,
|
||||
SiShadcnui,
|
||||
SiTailwindcss
|
||||
} from "react-icons/si"
|
||||
import Link from 'next/link'
|
||||
import { useState, useEffect } from 'react'
|
||||
|
||||
export const footerMessages = [
|
||||
[
|
||||
|
@ -33,6 +31,11 @@ export const footerMessages = [
|
|||
"https://vercel.com/font",
|
||||
<SiVercel key="vercel" className="text-md mr-2" />
|
||||
],
|
||||
[
|
||||
"Hosted by Cloudflare",
|
||||
"https://workers.cloudflare.com/",
|
||||
<SiCloudflarepages key="cloudflare" className="text-md mr-2" />
|
||||
],
|
||||
[
|
||||
"Icons by Font Awesome",
|
||||
"https://fontawesome.com/",
|
||||
|
@ -51,30 +54,11 @@ export const footerMessages = [
|
|||
]
|
||||
|
||||
export default function RandomFooterMsg() {
|
||||
const [randomIndex, setRandomIndex] = useState(0)
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
setRandomIndex(Math.floor(Math.random() * footerMessages.length))
|
||||
}, [])
|
||||
|
||||
if (!isMounted) {
|
||||
const [message, url, icon] = footerMessages[0]
|
||||
return (
|
||||
<Link href={String(url)} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
|
||||
<div className="flex items-center justify-center">
|
||||
{icon}
|
||||
{message}
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const randomIndex = Math.floor(Math.random() * footerMessages.length)
|
||||
const [message, url, icon] = footerMessages[randomIndex]
|
||||
|
||||
return (
|
||||
<Link href={String(url)} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0">
|
||||
<Link href={String(url)} target="_blank" rel="noopener noreferrer" className="hover:text-white transition-colors mb-2 sm:mb-0" suppressHydrationWarning>
|
||||
<div className="flex items-center justify-center">
|
||||
{icon}
|
||||
{message}
|
||||
|
|
47
components/objects/ScrollTxt.tsx
Normal file
47
components/objects/ScrollTxt.tsx
Normal file
|
@ -0,0 +1,47 @@
|
|||
"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">•</span>
|
||||
<span>{text}</span>
|
||||
<span className="mx-4">•</span>
|
||||
<span>{text}</span>
|
||||
</>
|
||||
) : (
|
||||
text
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default ScrollTxt
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import React from 'react'
|
||||
import BackButton from '@/components/objects/BackButton'
|
||||
|
||||
const WhatWasGoingOnLateSummer2024: React.FC = () => {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
|
||||
What was going on during the start of summer 2024?
|
||||
</h1>
|
||||
<div className="px-6 pt-6">
|
||||
<p className="text-gray-300 mb-4">
|
||||
During Early Summer 2024, I was walking a ton in towns all across Massachusetts. During this time, I would listen to a <i>lot</i> of music. I regret not finding out about LastFM for so long... During this time, I was always happy, especially when I had music or a YouTube video playing. I would also call my friends often during this time.
|
||||
</p>
|
||||
<h2 className="text-2xl font-semibold mb-4 mt-12 text-gray-200">Context</h2>
|
||||
<p className="text-gray-300 mb-4">
|
||||
This summer was the one where I came back from my abusive treatment center. I was finally free from the place that had been holding me back for so long. So as you can imagine, I felt free as a bird.
|
||||
</p>
|
||||
<p className="text-gray-300 mb-4">
|
||||
With this chance to explore, being in so many different towns, I really had a good time and made good memories, which I will not be writing about.
|
||||
</p>
|
||||
<BackButton href="/music" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatWasGoingOnLateSummer2024;
|
|
@ -0,0 +1,26 @@
|
|||
import React from 'react'
|
||||
import BackButton from '@/components/objects/BackButton'
|
||||
|
||||
const WhatWasGoingOnLateSummer2024: React.FC = () => {
|
||||
return (
|
||||
<div className="max-w-2xl mx-auto text-center">
|
||||
<h1 className="text-4xl font-bold my-2 text-center text-gray-200" style={{ textShadow: '0 0 10px rgba(255, 255, 255, 0.5)' }}>
|
||||
What was going on during the end of summer 2024?
|
||||
</h1>
|
||||
<div className="px-6 pt-6">
|
||||
<p className="text-gray-300 mb-4">
|
||||
During late summer 2024, my depression and the "after effects" of treatment really kicked in. I had quit going to my therapist as I didn't feel like they were doing much of anything for me. I am very happy to say that since I quit my therapist, I have been doing much better.
|
||||
</p>
|
||||
<p className="text-gray-300 mb-4">
|
||||
At this time, the baseball season was over, so I was walking around much less. I was still listening to a lot of music and I started getting into less depressed songs. I was also starting to get into more "normal" music, which was an interesting phase (which I believe I'm still in).
|
||||
</p>
|
||||
<p className="text-gray-300 mb-4">
|
||||
A highlight of late summer 2024 was a vacation I took. This vacation has entire albums which remind me of it and I will always cherish those memories deeply.
|
||||
</p>
|
||||
<BackButton href="/music" />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default WhatWasGoingOnLateSummer2024;
|
|
@ -1,8 +1,9 @@
|
|||
import { SiGithub, SiForgejo } from "react-icons/si"
|
||||
import { TbStar, TbGitBranch } from "react-icons/tb"
|
||||
import featuredProjects from "@/public/data/featured.json"
|
||||
import Link from "next/link"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'
|
||||
import { faGitAlt, faGithub } from '@fortawesome/free-brands-svg-icons'
|
||||
import { faStar, faCodeBranch } from '@fortawesome/free-solid-svg-icons'
|
||||
import featuredProjects from '@/public/data/featured.json'
|
||||
import Link from 'next/link'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export default function GitHubFeatured({ className }: { className?: string }) {
|
||||
return (
|
||||
|
@ -10,16 +11,16 @@ export default function GitHubFeatured({ className }: { className?: string }) {
|
|||
{featuredProjects.map((project) => (
|
||||
<div key={project.id} className="bg-gray-800 p-6 rounded-lg shadow-md min-h-[200px] flex flex-col">
|
||||
<div className="flex-1">
|
||||
<h3 className="flex items-center justify-center text-xl font-bold text-gray-100 mb-3">
|
||||
{project.github ? <SiGithub className="mr-2" /> : <SiForgejo className="mr-2" />} {project.name}
|
||||
<h3 className="text-xl font-bold text-gray-100 mb-3">
|
||||
<FontAwesomeIcon icon={project.github ? faGithub : faGitAlt} className="mr-2" /> {project.name}
|
||||
</h3>
|
||||
<p className="text-gray-300 grow">{project.description}</p>
|
||||
</div>
|
||||
<div className="pt-4 border-t border-gray-700 flex justify-between items-center mt-auto">
|
||||
<Link href={project.url} className="text-blue-400 hover:underline">View Repo</Link>
|
||||
<div className="flex items-center text-gray-400">
|
||||
<TbStar className="mr-1 size-5" /> {project.stars}
|
||||
<TbGitBranch className="ml-4 mr-1 size-5" /> {project.forks}
|
||||
<FontAwesomeIcon icon={faStar} className="mr-1" /> {project.stars}
|
||||
<FontAwesomeIcon icon={faCodeBranch} className="ml-4 mr-1" /> {project.forks}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
127
components/widgets/Music.tsx
Normal file
127
components/widgets/Music.tsx
Normal file
|
@ -0,0 +1,127 @@
|
|||
"use client"
|
||||
|
||||
import { useState, useEffect } from "react"
|
||||
import Image from "next/image"
|
||||
import { Play, SkipBack, SkipForward } from "lucide-react"
|
||||
import LoadingSpinner from "../objects/LoadingSpinner"
|
||||
import { SeekBar } from "@/components/objects/SeekBar"
|
||||
|
||||
interface Song {
|
||||
albumArt: string
|
||||
name: string
|
||||
artist: string
|
||||
duration: string
|
||||
link?: string
|
||||
}
|
||||
|
||||
interface Period {
|
||||
timePeriod: string
|
||||
songs: Song[]
|
||||
}
|
||||
|
||||
export default function Home() {
|
||||
const [timePeriod, setTimePeriod] = useState("Early Summer 2024")
|
||||
const [songs, setSongs] = useState<Song[]>([])
|
||||
const [currentIndex, setCurrentIndex] = useState(0)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [currentPosition, setCurrentPosition] = useState(0)
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(true)
|
||||
fetch("/data/music.json")
|
||||
.then((response) => response.json())
|
||||
.then((data: Period[]) => {
|
||||
const selectedPeriod = data.find((period) => period.timePeriod === timePeriod)
|
||||
const songsList = selectedPeriod ? selectedPeriod.songs : []
|
||||
setSongs(songsList)
|
||||
const newIndex = Math.floor(Math.random() * songsList.length)
|
||||
setCurrentIndex(newIndex)
|
||||
// Set initial random position for the selected song
|
||||
if (songsList.length > 0) {
|
||||
const durationInSeconds = parseDuration(songsList[newIndex]?.duration || "0:00")
|
||||
setCurrentPosition(Math.floor(Math.random() * durationInSeconds))
|
||||
}
|
||||
setIsLoading(false)
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching music data:", error)
|
||||
setIsLoading(false)
|
||||
})
|
||||
}, [timePeriod])
|
||||
|
||||
const handleNext = () => {
|
||||
setCurrentIndex((prevIndex) => {
|
||||
const nextIndex = (prevIndex + 1) % songs.length
|
||||
const durationInSeconds = parseDuration(songs[nextIndex].duration)
|
||||
setCurrentPosition(Math.floor(Math.random() * durationInSeconds))
|
||||
return nextIndex
|
||||
})
|
||||
}
|
||||
|
||||
const handlePrevious = () => {
|
||||
setCurrentIndex((prevIndex) => {
|
||||
const nextIndex = (prevIndex - 1 + songs.length) % songs.length
|
||||
const durationInSeconds = parseDuration(songs[nextIndex].duration)
|
||||
setCurrentPosition(Math.floor(Math.random() * durationInSeconds))
|
||||
return nextIndex
|
||||
})
|
||||
}
|
||||
|
||||
const parseDuration = (duration: string): number => {
|
||||
const [minutes, seconds] = duration.split(":").map(Number)
|
||||
return minutes * 60 + seconds
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<section id="music-carousel" className="mb-12">
|
||||
{isLoading && <LoadingSpinner />}
|
||||
|
||||
{!isLoading && songs.length > 0 && (
|
||||
<div className="relative">
|
||||
<Image
|
||||
src={songs[currentIndex].albumArt || "/placeholder.svg"}
|
||||
alt={songs[currentIndex].name}
|
||||
width={300}
|
||||
height={300}
|
||||
className="mb-4 rounded-lg"
|
||||
/>
|
||||
<h3 className="text-2xl font-bold text-gray-100">{songs[currentIndex].name}</h3>
|
||||
<p>{songs[currentIndex].artist}</p>
|
||||
<SeekBar
|
||||
key={`${currentIndex}-${currentPosition}`}
|
||||
startPos={currentPosition}
|
||||
duration={songs[currentIndex].duration}
|
||||
/>
|
||||
<div className="flex justify-center pb-2">
|
||||
<button onClick={handlePrevious} className="mr-4 cursor-pointer">
|
||||
<SkipBack className="w-8 h-8" />
|
||||
</button>
|
||||
<button className="mr-4 cursor-pointer" onClick={() => window.open(songs[currentIndex]?.link, "_blank")}>
|
||||
<Play className="w-8 h-8" />
|
||||
</button>
|
||||
<button onClick={handleNext} className="cursor-pointer">
|
||||
<SkipForward className="w-8 h-8" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-col items-center mt-4">
|
||||
<label htmlFor="timePeriod" className="font-bold uppercase text-sm pb-1">
|
||||
Time Period
|
||||
</label>
|
||||
<select
|
||||
id="timePeriod"
|
||||
value={timePeriod}
|
||||
onChange={(e) => setTimePeriod(e.target.value)}
|
||||
className="px-3 py-2 bg-gray-700 rounded-sm mb-2"
|
||||
>
|
||||
<option value="Early Summer 2024">Early Summer 2024</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,17 +1,13 @@
|
|||
"use client"
|
||||
|
||||
import type React from "react"
|
||||
import { useEffect, useState, useCallback } from "react"
|
||||
import { useEffect, useState, useCallback, useRef } from "react"
|
||||
import Image from "next/image"
|
||||
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 { Music, ExternalLink, Disc, User, Loader2, AlertCircle } from "lucide-react"
|
||||
import { TbDiscOff, TbDisc } from "react-icons/tb"
|
||||
import Marquee from "react-fast-marquee"
|
||||
import { Progress } from "@/components/ui/progress"
|
||||
import Link from "@/components/objects/Link"
|
||||
import ScrollTxt from "@/components/objects/MusicText"
|
||||
|
||||
interface Track {
|
||||
track_name: string
|
||||
|
@ -20,6 +16,34 @@ 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">•</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)
|
||||
|
@ -28,9 +52,6 @@ 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)
|
||||
|
@ -120,199 +141,115 @@ 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) {
|
||||
return (
|
||||
<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) {
|
||||
return (
|
||||
<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) {
|
||||
return (
|
||||
<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="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>
|
||||
)
|
||||
}
|
||||
|
||||
// normal state
|
||||
const currentTrack = track!;
|
||||
if (loading) {
|
||||
console.log("[LastPlayed] Loading state rendered")
|
||||
return (
|
||||
<>
|
||||
<a
|
||||
href={currentTrack.mbid ? `https://musicbrainz.org/release/${currentTrack.mbid}` : `https://listenbrainz.org/user/p0ntus`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="bg-gradient-to-b from-gray-700 to-gray-900 border-b border-gray-700 px-2 py-0 block" style={{background: 'linear-gradient(to bottom, #4b5563 0%, #374151 30%, #1f2937 70%, #111827 100%)'}}
|
||||
>
|
||||
<div className="text-center leading-none pb-1">
|
||||
<ScrollTxt text={currentTrack.artist_name.toUpperCase()} type="artist" />
|
||||
<ScrollTxt text={currentTrack.track_name} type="track" className="-mt-0.5" />
|
||||
{currentTrack.release_name && <ScrollTxt text={currentTrack.release_name} type="release" />}
|
||||
</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 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>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
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's playing right now</h2>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<p>Can you believe it? I'm not listening to anything on ListenBrainz right now! If you'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>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
console.log("[LastPlayed] Rendered track:", track.track_name)
|
||||
return (
|
||||
<div className="flex justify-center items-center">
|
||||
<div className="relative w-52 h-95 bg-[#D4C29A] rounded-xs border border-[#BFAF8A] z-10">
|
||||
{/* 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 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>
|
||||
{/* 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 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`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-400 flex items-center mt-1 hover:text-blue-300 transition-colors duration-200"
|
||||
>
|
||||
<ExternalLink size={16} className="mr-1 flex-shrink-0" />
|
||||
<span>View on MusicBrainz</span>
|
||||
</a>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
export default NowPlaying
|
||||
export default NowPlaying
|
2048
package-lock.json
generated
2048
package-lock.json
generated
File diff suppressed because it is too large
Load diff
38
package.json
38
package.json
|
@ -9,34 +9,38 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-progress": "^1.1.7",
|
||||
"@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",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"geist": "^1.4.2",
|
||||
"i18next": "^24.2.3",
|
||||
"i18next-browser-languagedetector": "^8.2.0",
|
||||
"i18next-browser-languagedetector": "^8.1.0",
|
||||
"lucide-react": "^0.485.0",
|
||||
"next": "^15.5.2",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-i18next": "^15.7.3",
|
||||
"next": "^15.3.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-fast-marquee": "^1.6.5",
|
||||
"react-i18next": "^15.5.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"recharts": "^3.1.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwind-merge": "^3.2.0",
|
||||
"tailwindcss-animate": "^1.0.7",
|
||||
"tw-animate-css": "^1.3.7"
|
||||
"tw-animate-css": "^1.2.9"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/eslintrc": "^3.3.1",
|
||||
"@tailwindcss/postcss": "^4.1.12",
|
||||
"@types/node": "^20.19.11",
|
||||
"@types/react": "^19.1.12",
|
||||
"@types/react-dom": "^19.1.9",
|
||||
"eslint": "^9.34.0",
|
||||
"@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",
|
||||
"eslint-config-next": "15.1.3",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.12",
|
||||
"typescript": "^5.9.2"
|
||||
"postcss": "^8.5.3",
|
||||
"tailwindcss": "^4.1.6",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"trustedDependencies": [
|
||||
"sharp",
|
||||
|
|
|
@ -1,587 +0,0 @@
|
|||
{
|
||||
"daily": [
|
||||
{
|
||||
"date": "2025-08-08",
|
||||
"inputTokens": 14919,
|
||||
"outputTokens": 23378,
|
||||
"cacheCreationTokens": 480031,
|
||||
"cacheReadTokens": 11034031,
|
||||
"totalTokens": 11552359,
|
||||
"totalCost": 6.777273749999996,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-20250514"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 4837,
|
||||
"outputTokens": 20788,
|
||||
"cacheCreationTokens": 443453,
|
||||
"cacheReadTokens": 10661975,
|
||||
"cost": 5.18787225
|
||||
},
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 10082,
|
||||
"outputTokens": 2590,
|
||||
"cacheCreationTokens": 36578,
|
||||
"cacheReadTokens": 372056,
|
||||
"cost": 1.5894014999999997
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-09",
|
||||
"inputTokens": 3142,
|
||||
"outputTokens": 20594,
|
||||
"cacheCreationTokens": 513312,
|
||||
"cacheReadTokens": 13270007,
|
||||
"totalTokens": 13807055,
|
||||
"totalCost": 20.561232300000007,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 373,
|
||||
"outputTokens": 10485,
|
||||
"cacheCreationTokens": 294339,
|
||||
"cacheReadTokens": 7740261,
|
||||
"cost": 17.92121775
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 2769,
|
||||
"outputTokens": 10109,
|
||||
"cacheCreationTokens": 218973,
|
||||
"cacheReadTokens": 5529746,
|
||||
"cost": 2.640014549999999
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-10",
|
||||
"inputTokens": 2384,
|
||||
"outputTokens": 33087,
|
||||
"cacheCreationTokens": 752268,
|
||||
"cacheReadTokens": 12833548,
|
||||
"totalTokens": 13621287,
|
||||
"totalCost": 24.83825640000001,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-20250514"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 983,
|
||||
"outputTokens": 24065,
|
||||
"cacheCreationTokens": 320876,
|
||||
"cacheReadTokens": 9495745,
|
||||
"cost": 22.079662499999998
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 1401,
|
||||
"outputTokens": 9022,
|
||||
"cacheCreationTokens": 431392,
|
||||
"cacheReadTokens": 3337803,
|
||||
"cost": 2.7585938999999993
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-11",
|
||||
"inputTokens": 1127,
|
||||
"outputTokens": 23663,
|
||||
"cacheCreationTokens": 746606,
|
||||
"cacheReadTokens": 10310633,
|
||||
"totalTokens": 11082029,
|
||||
"totalCost": 31.256441999999993,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 1127,
|
||||
"outputTokens": 23663,
|
||||
"cacheCreationTokens": 746606,
|
||||
"cacheReadTokens": 10310633,
|
||||
"cost": 31.256441999999993
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-12",
|
||||
"inputTokens": 17245,
|
||||
"outputTokens": 164864,
|
||||
"cacheCreationTokens": 2646250,
|
||||
"cacheReadTokens": 49767559,
|
||||
"totalTokens": 52595918,
|
||||
"totalCost": 85.49760780000005,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-20250514"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 13710,
|
||||
"outputTokens": 77330,
|
||||
"cacheCreationTokens": 1413354,
|
||||
"cacheReadTokens": 26762148,
|
||||
"cost": 72.64900950000008
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 3535,
|
||||
"outputTokens": 87534,
|
||||
"cacheCreationTokens": 1232896,
|
||||
"cacheReadTokens": 23005411,
|
||||
"cost": 12.848598300000004
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-13",
|
||||
"inputTokens": 29365,
|
||||
"outputTokens": 23237,
|
||||
"cacheCreationTokens": 1034891,
|
||||
"cacheReadTokens": 7332169,
|
||||
"totalTokens": 8419662,
|
||||
"totalCost": 9.039594749999997,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 24909,
|
||||
"outputTokens": 18462,
|
||||
"cacheCreationTokens": 935307,
|
||||
"cacheReadTokens": 6758235,
|
||||
"cost": 5.886528749999999
|
||||
},
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 4456,
|
||||
"outputTokens": 4775,
|
||||
"cacheCreationTokens": 99584,
|
||||
"cacheReadTokens": 573934,
|
||||
"cost": 3.1530659999999995
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-14",
|
||||
"inputTokens": 4984,
|
||||
"outputTokens": 20654,
|
||||
"cacheCreationTokens": 676409,
|
||||
"cacheReadTokens": 8769252,
|
||||
"totalTokens": 9471299,
|
||||
"totalCost": 13.503454350000002,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 282,
|
||||
"outputTokens": 6364,
|
||||
"cacheCreationTokens": 260989,
|
||||
"cacheReadTokens": 3092770,
|
||||
"cost": 10.014228749999996
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 4702,
|
||||
"outputTokens": 14290,
|
||||
"cacheCreationTokens": 415420,
|
||||
"cacheReadTokens": 5676482,
|
||||
"cost": 3.489225600000001
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-15",
|
||||
"inputTokens": 6744,
|
||||
"outputTokens": 53509,
|
||||
"cacheCreationTokens": 1315474,
|
||||
"cacheReadTokens": 18699807,
|
||||
"totalTokens": 20075534,
|
||||
"totalCost": 37.771287000000015,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-20250514"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 3739,
|
||||
"outputTokens": 36231,
|
||||
"cacheCreationTokens": 818888,
|
||||
"cacheReadTokens": 9919502,
|
||||
"cost": 33.00681300000001
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 3005,
|
||||
"outputTokens": 17278,
|
||||
"cacheCreationTokens": 496586,
|
||||
"cacheReadTokens": 8780305,
|
||||
"cost": 4.764474
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-16",
|
||||
"inputTokens": 67226,
|
||||
"outputTokens": 230912,
|
||||
"cacheCreationTokens": 4693459,
|
||||
"cacheReadTokens": 126251857,
|
||||
"totalTokens": 131243454,
|
||||
"totalCost": 67.51195695000014,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 66858,
|
||||
"outputTokens": 225159,
|
||||
"cacheCreationTokens": 4442992,
|
||||
"cacheReadTokens": 122698549,
|
||||
"cost": 57.048743700000024
|
||||
},
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 368,
|
||||
"outputTokens": 5753,
|
||||
"cacheCreationTokens": 250467,
|
||||
"cacheReadTokens": 3553308,
|
||||
"cost": 10.463213249999994
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-17",
|
||||
"inputTokens": 5258,
|
||||
"outputTokens": 107279,
|
||||
"cacheCreationTokens": 2065168,
|
||||
"cacheReadTokens": 40221095,
|
||||
"totalTokens": 42398800,
|
||||
"totalCost": 46.292036099999905,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 1306,
|
||||
"outputTokens": 37998,
|
||||
"cacheCreationTokens": 568961,
|
||||
"cacheReadTokens": 11688668,
|
||||
"cost": 31.07046074999999
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 3952,
|
||||
"outputTokens": 69281,
|
||||
"cacheCreationTokens": 1496207,
|
||||
"cacheReadTokens": 28532427,
|
||||
"cost": 15.221575350000005
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-18",
|
||||
"inputTokens": 26822,
|
||||
"outputTokens": 158126,
|
||||
"cacheCreationTokens": 4162794,
|
||||
"cacheReadTokens": 85133032,
|
||||
"totalTokens": 89480774,
|
||||
"totalCost": 98.36223149999994,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 1334,
|
||||
"outputTokens": 59577,
|
||||
"cacheCreationTokens": 1226868,
|
||||
"cacheReadTokens": 27304867,
|
||||
"cost": 68.4493605
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 25488,
|
||||
"outputTokens": 98549,
|
||||
"cacheCreationTokens": 2935926,
|
||||
"cacheReadTokens": 57828165,
|
||||
"cost": 29.912871000000024
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-19",
|
||||
"inputTokens": 25035,
|
||||
"outputTokens": 192422,
|
||||
"cacheCreationTokens": 2749046,
|
||||
"cacheReadTokens": 86412205,
|
||||
"totalTokens": 89378708,
|
||||
"totalCost": 120.90988019999996,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 6500,
|
||||
"outputTokens": 120014,
|
||||
"cacheCreationTokens": 1447294,
|
||||
"cacheReadTokens": 43939676,
|
||||
"cost": 102.14482650000001
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 18535,
|
||||
"outputTokens": 72408,
|
||||
"cacheCreationTokens": 1301752,
|
||||
"cacheReadTokens": 42472529,
|
||||
"cost": 18.76505370000004
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-20",
|
||||
"inputTokens": 1777,
|
||||
"outputTokens": 45019,
|
||||
"cacheCreationTokens": 1288952,
|
||||
"cacheReadTokens": 18847679,
|
||||
"totalTokens": 20183427,
|
||||
"totalCost": 46.130642700000024,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805",
|
||||
"claude-sonnet-4-20250514"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 1472,
|
||||
"outputTokens": 29172,
|
||||
"cacheCreationTokens": 1017913,
|
||||
"cacheReadTokens": 14937895,
|
||||
"cost": 43.702691249999994
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 305,
|
||||
"outputTokens": 15847,
|
||||
"cacheCreationTokens": 271039,
|
||||
"cacheReadTokens": 3909784,
|
||||
"cost": 2.4279514499999997
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-21",
|
||||
"inputTokens": 81,
|
||||
"outputTokens": 3400,
|
||||
"cacheCreationTokens": 57191,
|
||||
"cacheReadTokens": 406935,
|
||||
"totalTokens": 467607,
|
||||
"totalCost": 1.0935505500000002,
|
||||
"modelsUsed": [
|
||||
"claude-sonnet-4-20250514",
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 37,
|
||||
"outputTokens": 826,
|
||||
"cacheCreationTokens": 29950,
|
||||
"cacheReadTokens": 172089,
|
||||
"cost": 0.882201
|
||||
},
|
||||
{
|
||||
"modelName": "claude-sonnet-4-20250514",
|
||||
"inputTokens": 44,
|
||||
"outputTokens": 2574,
|
||||
"cacheCreationTokens": 27241,
|
||||
"cacheReadTokens": 234846,
|
||||
"cost": 0.21134955
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"date": "2025-08-22",
|
||||
"inputTokens": 201,
|
||||
"outputTokens": 26357,
|
||||
"cacheCreationTokens": 182770,
|
||||
"cacheReadTokens": 1764101,
|
||||
"totalTokens": 1973429,
|
||||
"totalCost": 8.052878999999999,
|
||||
"modelsUsed": [
|
||||
"claude-opus-4-1-20250805"
|
||||
],
|
||||
"modelBreakdowns": [
|
||||
{
|
||||
"modelName": "claude-opus-4-1-20250805",
|
||||
"inputTokens": 201,
|
||||
"outputTokens": 26357,
|
||||
"cacheCreationTokens": 182770,
|
||||
"cacheReadTokens": 1764101,
|
||||
"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": {
|
||||
"inputTokens": 210111,
|
||||
"outputTokens": 1247590,
|
||||
"cacheCreationTokens": 25422815,
|
||||
"cacheReadTokens": 522838726,
|
||||
"totalCost": 710.6193022500001,
|
||||
"totalTokens": 549719242
|
||||
}
|
||||
}
|
|
@ -6,23 +6,23 @@
|
|||
},
|
||||
{
|
||||
"id": 2,
|
||||
"domain": "pontushost.com",
|
||||
"usage": "My hosting provider project"
|
||||
"domain": "aidxn.fun",
|
||||
"usage": "My alternative homepage (Version 2)"
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"domain": "librecloud.cc",
|
||||
"usage": "LibreCloud's root domain"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"domain": "disfunction.blog",
|
||||
"usage": "My blog's official home"
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"domain": "androidintegrity.org",
|
||||
"usage": "A project to fix Play Integrity"
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"domain": "librecloud.cc",
|
||||
"usage": "My old cloud services provider project"
|
||||
"domain": "androidintegrity.org",
|
||||
"usage": "A team project to improve Play Integrity"
|
||||
},
|
||||
{
|
||||
"id": 6,
|
||||
|
@ -47,12 +47,12 @@
|
|||
{
|
||||
"id": 10,
|
||||
"domain": "dontbeevil.lol",
|
||||
"usage": "Another fun domain for p0ntus mail"
|
||||
"usage": "A Google meme domain used for p0ntus mail"
|
||||
},
|
||||
{
|
||||
"id": 11,
|
||||
"domain": "wikitools.cloud",
|
||||
"usage": "Unused (for now!)"
|
||||
"usage": "Tools I've made for Wikipedia"
|
||||
},
|
||||
{
|
||||
"id": 12,
|
||||
|
|
|
@ -4,35 +4,35 @@
|
|||
"name": "aidxnCC",
|
||||
"description": "aidxnCC is the third version of my personal website",
|
||||
"github": false,
|
||||
"url": "https://git.p0ntus.com/aidan/aidxnCC",
|
||||
"stars": 1,
|
||||
"forks": 0
|
||||
"url": "https://git.pontusmail.org/aidan/aidxnCC",
|
||||
"stars": 2,
|
||||
"forks": 1
|
||||
},
|
||||
{
|
||||
"id": 2,
|
||||
"name": "abocn/TelegramBot",
|
||||
"name": "librecloud/web",
|
||||
"description": "Landing page for p0ntus mail",
|
||||
"github": true,
|
||||
"url": "https://github.com/abocn/TelegramBot",
|
||||
"stars": 13,
|
||||
"forks": 6
|
||||
"github": false,
|
||||
"url": "https://git.pontusmail.org/librecloud/web",
|
||||
"stars": 0,
|
||||
"forks": 0
|
||||
},
|
||||
{
|
||||
"id": 3,
|
||||
"name": "modules",
|
||||
"description": "A Magisk/KernelSU module repository",
|
||||
"github": true,
|
||||
"url": "https://github.com/abocn/modules",
|
||||
"stars": 5,
|
||||
"description": "An open-source Magisk module and FOSS app store",
|
||||
"github": false,
|
||||
"url": "https://git.pontusmail.org/aidan/modules",
|
||||
"stars": 3,
|
||||
"forks": 0
|
||||
},
|
||||
{
|
||||
"id": 4,
|
||||
"name": "pontus/pontus-front",
|
||||
"description": "The frontend and API for p0ntus, my free privacy-focused service provider",
|
||||
"github": false,
|
||||
"url": "https://git.p0ntus.com/pontus/pontus-front",
|
||||
"stars": 1,
|
||||
"forks": 0
|
||||
"name": "AndroidIntegrity/website",
|
||||
"description": "AIA website source code",
|
||||
"github": true,
|
||||
"url": "https://github.com/AndroidIntegrity/website",
|
||||
"stars": 6,
|
||||
"forks": 1
|
||||
}
|
||||
]
|
||||
|
|
56
public/data/music.json
Normal file
56
public/data/music.json
Normal file
|
@ -0,0 +1,56 @@
|
|||
[
|
||||
{
|
||||
"timePeriod": "Early Summer 2024",
|
||||
"songs": [
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/noticeme.png",
|
||||
"name": "Notice Me",
|
||||
"artist": "tobi lou feat. MIA GLADSTONE",
|
||||
"duration": "2:35",
|
||||
"link": "https://www.last.fm/music/tobi+lou/Notice+Me"
|
||||
},
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/comforttexas.webp",
|
||||
"name": "comfort, texas",
|
||||
"artist": "Buppy.",
|
||||
"duration": "2:11",
|
||||
"link": "https://www.last.fm/music/Buppy./comfort,+texas"
|
||||
},
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/nonperishable.webp",
|
||||
"name": "Jelly",
|
||||
"artist": "tobi lou",
|
||||
"duration": "1:50",
|
||||
"link": "https://www.last.fm/music/tobi+lou/_/Jelly"
|
||||
},
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/exes.webp",
|
||||
"name": "exes",
|
||||
"artist": "Tate McRae",
|
||||
"duration": "2:39",
|
||||
"link": "https://www.last.fm/music/Tate+McRae/exes/exes"
|
||||
},
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/ick.webp",
|
||||
"name": "Ick",
|
||||
"artist": "Lay Bankz",
|
||||
"duration": "1:55",
|
||||
"link": "https://www.last.fm/music/Lay+Bankz/_/Ick"
|
||||
},
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/nani.webp",
|
||||
"name": "NANi",
|
||||
"artist": "Saweetie",
|
||||
"duration": "2:34",
|
||||
"link": "https://www.last.fm/music/Saweetie/Nani"
|
||||
},
|
||||
{
|
||||
"albumArt": "https://p0ntus.com/archives/img/killerloverboy.webp",
|
||||
"name": "killer lover boy",
|
||||
"artist": "SEB",
|
||||
"duration": "2:14",
|
||||
"link": "https://www.last.fm/music/Seb/_/killer+lover+boy"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
Binary file not shown.
Before Width: | Height: | Size: 41 KiB |
Binary file not shown.
Before Width: | Height: | Size: 39 KiB |
|
@ -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 Boston area. I primarily work with Linux, Docker, Next.js, Tailwind CSS and TypeScript.",
|
||||
"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!",
|
||||
"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."
|
||||
"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.",
|
||||
"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 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."
|
||||
"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.\""
|
||||
],
|
||||
"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 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."
|
||||
"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 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.",
|
||||
"This page is currently hosted on Cloudflare Workers, after what happened with "
|
||||
],
|
||||
"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! I aim to answer all of my messages in a timely fashion, but please have patience.",
|
||||
"description": "Feel free to reach out for feedback, collaborations, or just a hello :)",
|
||||
"button": "Contact Me"
|
||||
},
|
||||
"donation": {
|
||||
|
@ -31,10 +31,10 @@
|
|||
"description": "Feeling generous? Support me or one of the causes I support!",
|
||||
"charities": {
|
||||
"title": "Charities",
|
||||
"description": "I support the following charities:",
|
||||
"unsilenced": "Unsilenced",
|
||||
"drugpolicy": "Drug Policy Alliance",
|
||||
"aclu": "ACLU",
|
||||
"epic-restart": "EPIC Restart Foundation"
|
||||
"aclu": "ACLU"
|
||||
},
|
||||
"donate": {
|
||||
"title": "Donate to Me",
|
||||
|
@ -67,6 +67,14 @@
|
|||
"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": {
|
||||
|
@ -80,37 +88,31 @@
|
|||
"featuredProjects": "Featured Projects"
|
||||
},
|
||||
"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."
|
||||
"I have worked on countless projects over the past five years, for the most part. I have been learning to program in Python since I was seven and have 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 Node.js, my new favorite language as of a year ago. My biggest project is LibreCloud, a free service provider for individuals.",
|
||||
"In terms of system administration, I have developed my skills over the past three years of learning Linux for fun. I currently operate three servers running in the cloud, which run out of Germany and the United States."
|
||||
],
|
||||
"hobbies": [
|
||||
"When I'm not programming, I can typically be found distro hopping or flashing a new ROM to my phone. I also spend a lot of time spreading Next.js and TypeScript propaganda to JavaScript developers.",
|
||||
"I consider maintaining my devices as a hobby as well, as I devote a lot of time to it. I genuinely enjoy installing Arch, Gentoo, and NixOS frequently, and flashing new ROMs to the phones I own.",
|
||||
"I am frequently active on my Forgejo server and GitHub, and aim to make daily contributions. I am a big fan of open source software and public domain software (which most of my repos are licensed under). In fact, the website you're currently on is free and open source. It's even under the public domain!",
|
||||
"When I touch grass, I prefer to walk on the streets, especially in Boston, Massachusetts. I also used to swim competitively, though it has turned into to a casual hobby over time.",
|
||||
"Editing Wikipedia has also been a good pastime for me, and I have been editing for a year and a half now. As of writing, I have made 6.1k edits to the English Wikipedia. I am also an AfC reviewer, new page reviewer, and rollbacker. You can find me on Wikipedia as OnlyNano."
|
||||
"When I'm not programming, I can typically be found installing another Linux distro on my laptop or flashing a new ROM to my phone. I am also a passionate writer and I like to write creatively in my free time.",
|
||||
"I consider maintaining my technology as a hobby as well, as I devote a lot of time to it. I currently run Gentoo Linux on my Thinkpad T470s, which does not use a single bin package. I am very proud of this laptop, despite it's constant need for compiling updates.",
|
||||
"I am almost always active on my Gitea instance and GitHub and make daily contributions to several of my repositories. I am a big fan of open source software and public domain software (which most of my repos are licensed under). In fact, the website you're currently on is free and open source. It's even under the public domain!",
|
||||
"My Google Pixel 7 Pro (cheetah) runs LineageOS 22.1, and has been one of my favorite additions to my life. It is proudly rooted with KernelSU-Next. It has suffered one drop to it's back on a tile floor."
|
||||
],
|
||||
"devices": {
|
||||
"Mobile Devices": [
|
||||
"I use a Google Pixel 9 Pro XL (komodo) as my daily driver. It runs Android 16 and is proudly rooted with KernelSU-Next.",
|
||||
"My previous phone, the Google Pixel 7 Pro (cheetah), is still in use as my secondary WiFi-only device. It runs Android 16 and is proudly rooted with KernelSU-Next.",
|
||||
"I also have a Google Pixel 3a XL (bonito) which I use as a tertiary device. It runs LineageOS 22.2 and is rooted with Magisk."
|
||||
"Phone": [
|
||||
"I use a Google Pixel 7 Pro (cheetah) as my daily driver. It runs LineageOS microG and is proudly rooted with KernelSU-Next.",
|
||||
"It's back is shattered and missing volume buttons, but it continues to thrive and survive as my daily driver."
|
||||
],
|
||||
"Laptops": [
|
||||
"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."
|
||||
"Laptop": [
|
||||
"I use a Lenovo Thinkpad T470s running Arch Linux. I've had it for about half a year now, and it's been a great experience. I proudly use X11 and LXDE, with some Xfce backend components to make management easier."
|
||||
]
|
||||
},
|
||||
"contributions": [
|
||||
"Most of my repositories have migrated to p0ntus git. My username is aidan. You can find me on GitHub as ihatenodejs."
|
||||
"Most of my repositories have migrated to LibreCloud Git. My username is aidan.",
|
||||
"You can find me on GitHub as ihatenodejs."
|
||||
],
|
||||
"featuredProjects": [
|
||||
"Here's just four of my top projects. Star and fork counts are manually updated and count both Gitea and GitHub."
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -6,6 +6,7 @@ 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}",
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue