diff --git a/.gitea/workflows/lint.yml b/.gitea/workflows/lint.yml new file mode 100644 index 0000000..cf3ac00 --- /dev/null +++ b/.gitea/workflows/lint.yml @@ -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 diff --git a/.gitea/workflows/push.yml b/.gitea/workflows/push.yml new file mode 100644 index 0000000..0ff437f --- /dev/null +++ b/.gitea/workflows/push.yml @@ -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 }} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 0e2d1be..61d6aa7 100644 --- a/Dockerfile +++ b/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 diff --git a/README.md b/README.md index 2300315..db7bed3 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,42 @@ # aidxnCC [![License: Unlicense](https://img.shields.io/badge/license-Unlicense-blue.svg)](http://unlicense.org/) +[![Build Status](https://git.pontusmail.org/aidan/aidxnCC/actions/workflows/push.yml/badge.svg)](https://git.pontusmail.org/aidan/aidxnCC/actions/?workflow=push.yml) +[![ESLint Status](https://git.pontusmail.org/aidan/aidxnCC/actions/workflows/lint.yml/badge.svg)](https://git.pontusmail.org/aidan/aidxnCC/actions/?workflow=lint.yml) aidxnCC is the third version of my personal website. It's built with Next.js and Tailwind CSS. aidxnCC will always be a work in progress, though completely functional. -## Deploy 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 +[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fihatenodejs%2FaidxnCC&env=BRAINZ_USER_AGENT,LISTENBRAINZ_TOKEN&envDescription=You%20will%20need%20both%20a%20custom%20user%20agent%20(for%20identifying%20yourself%20to%20MusicBrainz)%2C%20and%20a%20ListenBrainz%20User%20Token.%20See%20the%20README%20for%20more%20information.&envLink=https%3A%2F%2Fgithub.com%2Fihatenodejs%2FaidxnCC&project-name=aidxn-cc&repository-name=aidxnCC) -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 :) diff --git a/app/about/page.tsx b/app/about/page.tsx index 0b87daa..f0b4fed 100644 --- a/app/about/page.tsx +++ b/app/about/page.tsx @@ -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 (
-
-
-
- -
-

{t('about.title')}

+
+
+
+

+ {t('about.title')} +

{mainStrings.map((section, index) => { if (mainSections[index] === t('about.sections.featuredProjects')) { return ( -
+

{mainSections[index]}

{section.map((text, index) => (

@@ -57,19 +56,19 @@ export default function About() { ) } else if (mainSections[index] === t('about.sections.contributions')) { return ( -

+

{mainSections[index]}

{section.map((text, index) => (

- {text.split(/(ihatenodejs|p0ntus git|aidan)/).map((part, i) => { + {text.split(/(ihatenodejs|LibreCloud Git|aidan)/).map((part, i) => { if (part === 'ihatenodejs') { - return ihatenodejs + return GitHub } - if (part === 'p0ntus git') { - return p0ntus git + if (part === 'LibreCloud Git') { + return LibreCloud Git } if (part === 'aidan') { - return aidan + return aidan } return part })} @@ -77,19 +76,18 @@ export default function About() { ))} {!imageError && (

- ihatenodejs's Stats setImageError(true)} loading="eager" priority unoptimized - className="max-w-full h-auto" /> - ihatenodejs's Top Languages
)} @@ -105,54 +102,31 @@ export default function About() { ) } else if (mainSections[index] === t('about.sections.devices')) { return ( -
+

{mainSections[index]}

{Object.entries(section).map(([key, value], index) => (
-

{key}

+

{key}

{(value as unknown as string[]).map((text: string, index: number) => (

- {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 KernelSU-Next } - if (part === 'LineageOS 22.2') { - return LineageOS 22.2 - } - if (part === 'Android 16') { - return Android 16 - } - if (part === 'OpenCore') { - return OpenCore - } - if (part === 'Xubuntu') { - return Xubuntu + if (part === 'LineageOS microG') { + return LineageOS microG } return part })}

))} - {key === "Mobile Devices" && ( -
- - - -
+ {key === "Phone" && ( +
))} @@ -160,22 +134,16 @@ export default function About() { ) } else if (mainSections[index] === t('about.sections.hobbies')) { return ( -
+

{mainSections[index]}

{section.map((text, index) => (

- {text.split(/(my Forgejo server|my phone|AfC|OnlyNano)/).map((part, i) => { - if (part === 'my Forgejo server') { - return my Forgejo server + {text.split(/(my Gitea instance|my phone)/).map((part, i) => { + if (part === 'my Gitea instance') { + return my Gitea instance } if (part === 'my phone') { - return my phone - } - if (part === 'AfC') { - return AfC - } - if (part === 'OnlyNano') { - return OnlyNano + return my phone } return part })} @@ -185,25 +153,13 @@ export default function About() { ) } else if (mainSections[index] === t('about.sections.projects')) { return ( -

+

{mainSections[index]}

{section.map((text, index) => (

- {text.split(/(p0ntus|PontusHub|ABOCN|Kowalski|@KowalskiNodeBot)/).map((part, i) => { - if (part === 'p0ntus') { - return p0ntus - } - if (part === 'PontusHub') { - return PontusHub - } - if (part === 'ABOCN') { - return ABOCN - } - if (part === 'Kowalski') { - return Kowalski - } - if (part === '@KowalskiNodeBot') { - return @KowalskiNodeBot + {text.split(/(LibreCloud)/).map((part, i) => { + if (part === 'LibreCloud') { + return LibreCloud } return part })} @@ -213,7 +169,7 @@ export default function About() { ) } else { return ( -

+

{mainSections[index]}

{section.map((text, index) => (

diff --git a/app/ai/claude/page.tsx b/app/ai/claude/page.tsx deleted file mode 100644 index fb549da..0000000 --- a/app/ai/claude/page.tsx +++ /dev/null @@ -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(null) - const [loading, setLoading] = useState(true) - const [error, setError] = useState(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 ( -

-
-
- - ← Back to AI - - -
-
- -
-

Claude Code Usage

-

How much I use Claude Code!

-
- -
-
-

Total Cost

-
-
-
-

Total Tokens

-
-
-
-

Days Active

-
-
-
-

Avg Daily Cost

-
-
-
- -
-
-

Daily Usage Trend

-
- - -
-
-
-
-

Model Usage Distribution

-
-
-
-

Token Type Breakdown

-
-
-
-

Daily Token Composition

-
-
-
- -
-
-

Recent Sessions

-
- - - - - - - - - - - {[...Array(5)].map((_, index) => ( - - - - - - - ))} - -
DateModels UsedTotal TokensCost
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ) - } - - if (error || !data) { - return ( -
-
-
-
Error loading data: {error}
-
-
-
- ) - } - - 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 ( -
-
-
- - ← Back to AI - -
-
- -
-

Claude Code Usage

-

How much I use Claude Code!

-
- -
-
-

Total Cost

-

${data.totals.totalCost.toFixed(2)}

-
-
-

Total Tokens

-

{(data.totals.totalTokens / 1000000).toFixed(1)}M

-
-
-

Days Active

-

{data.daily.length}

-
-
-

Avg Daily Cost

-

${(data.totals.totalCost / data.daily.length).toFixed(2)}

-
-
- -
-
-

Daily Usage Trend

-
- - -
- - - - - - selectedMetric === 'cost' ? formatCurrency(value) : formatTokens(value)} - /> - - - -
- -
-

Model Usage Distribution

-
- - - - {modelUsageData.map((entry, index) => ( - - ))} - - formatCurrency(value)} - labelStyle={{ color: '#fff' }} - itemStyle={{ color: '#fff' }} - /> - - -
- {modelUsageData.map((model, index) => { - const percentage = ((model.value / data.totals.totalCost) * 100).toFixed(1) - return ( -
-
-
- {model.name} -
-
- {percentage}% - ${model.value.toFixed(2)} -
-
- ) - })} -
-
- Total Models Used - {modelUsageData.length} -
-
- Most Used - - {modelUsageData[0]?.name} - -
-
-
-
-
- -
-

Token Type Breakdown

- - - - - `${(value / 1000000).toFixed(0)}M`} /> - `${(value / 1000000).toFixed(2)}M tokens`} - /> - - - -
- -
-

Daily Token Composition

- - - - - `${value}K`} /> - `${value.toFixed(1)}K tokens`} - /> - - - - - - -
-
- -
-
-

Recent Sessions

-
- - - - - - - - - - - {data.daily.slice(-5).reverse().map((day, index) => ( - - - - - - - ))} - -
DateModels UsedTotal TokensCost
{new Date(day.date + 'T00:00:00').toLocaleDateString()} - {day.modelsUsed.join(', ')} - {(day.totalTokens / 1000000).toFixed(2)}M${day.totalCost.toFixed(2)}
-
-
-
-
-
-
- ) -} \ No newline at end of file diff --git a/app/ai/components/AIStack.tsx b/app/ai/components/AIStack.tsx deleted file mode 100644 index 7bf51d5..0000000 --- a/app/ai/components/AIStack.tsx +++ /dev/null @@ -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 ( -
-

- - My AI Stack -

-
- {tools.map((tool, index) => ( -
-
-
- {tool.icon && } - {tool.svg && ( -
- {tool.svg} -
- )} -
-

{tool.name}

-

{tool.description}

-
-
-
-
- - {getStatusLabel(tool.status)} - - - {tool.link && ( - - View → - - )} - {tool.usage && ( - - Usage → - - )} - -
-
- ))} -
-
- ) -} diff --git a/app/ai/components/FavoriteModels.tsx b/app/ai/components/FavoriteModels.tsx deleted file mode 100644 index 12ea967..0000000 --- a/app/ai/components/FavoriteModels.tsx +++ /dev/null @@ -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 ( -
-

- - Favorite Models -

-
- {models.map((model, index) => ( -
-
-
-

{model.name}

-

{model.provider}

-
-
- {[...Array(5)].map((_, i) => ( - - ))} -
-
-

{model.review}

-
- ))} -
-
- ) -} diff --git a/app/ai/components/ToolReviews.tsx b/app/ai/components/ToolReviews.tsx deleted file mode 100644 index 88d77a5..0000000 --- a/app/ai/components/ToolReviews.tsx +++ /dev/null @@ -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 ( -
-

- - Tool Reviews -

-
- {reviews.map((review, index) => ( -
-
-

{review.tool}

-
- {[...Array(5)].map((_, i) => ( - - ))} -
-
-
-
-

Pros:

-
    - {review.pros.map((pro, i) => ( -
  • • {pro}
  • - ))} -
-
-
-

Cons:

-
    - {review.cons.map((con, i) => ( -
  • • {con}
  • - ))} -
-
-
-

{review.verdict}

-
- ))} -
-
- ) -} diff --git a/app/ai/components/TopPick.tsx b/app/ai/components/TopPick.tsx deleted file mode 100644 index 2faccd3..0000000 --- a/app/ai/components/TopPick.tsx +++ /dev/null @@ -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 ( -
-

- - Top Pick of 2025 -

-
-
-
- -
-

Claude

-

by Anthropic

-
- - View My Usage - -
-
-
-
-

- 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. -

-
-
- Claude Code - Best Tool Calling - High Value in Max Plan - Quite Fast Interface -
-
- - Top Overall Pick - -
-
-
-
-
-
- ) -} diff --git a/app/ai/data.tsx b/app/ai/data.tsx deleted file mode 100644 index 1e121be..0000000 --- a/app/ai/data.tsx +++ /dev/null @@ -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: v0, - description: "Generating boilerplate UIs", - status: "occasional", - link: "https://v0.dev/" - }, - { - name: "Qwen", - 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" - }, -] \ No newline at end of file diff --git a/app/ai/page.tsx b/app/ai/page.tsx deleted file mode 100644 index 077b779..0000000 --- a/app/ai/page.tsx +++ /dev/null @@ -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 ( -
-
-
-
-
- -
-

AI

-

My journey with using LLMs

-
- - - -
- -
- -
- - -
-
-
-
- ) -} \ No newline at end of file diff --git a/app/ai/types.ts b/app/ai/types.ts deleted file mode 100644 index 034db85..0000000 --- a/app/ai/types.ts +++ /dev/null @@ -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; -} \ No newline at end of file diff --git a/app/contact/page.tsx b/app/contact/page.tsx index ee209ba..0edfd2f 100644 --- a/app/contact/page.tsx +++ b/app/contact/page.tsx @@ -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 = [ - , - , - , - , - - ]; + const contactButtonIcons = [faGithub, faTelegram, faBluesky, faXTwitter, faPhone, faEnvelope]; return (
-
-
- -
-

- {t('contact.title')} -

+
+
-
-
- {contactButtonLabels.map((label, index) => ( - - ))} -
- {sections.map((section, sectionIndex) => ( -
-

{section.title}

- {section.texts.map((text, index) => ( -

{text}

- ))} -
+

+ {t('contact.title')} +

+
+ {contactButtonLabels.map((label, index) => ( + ))}
+ + {sections.map((section, sectionIndex) => ( +
+

{section.title}

+ {section.texts.map((text, index) => ( +

{text}

+ ))} +
+ ))}