feat: add loading animation and UI components

This commit is contained in:
End 2025-02-01 12:27:00 -07:00
parent b681c58b88
commit 5772a7da3d
No known key found for this signature in database
25 changed files with 10925 additions and 379 deletions

View file

@ -1,4 +1,4 @@
# deploy.ps1
# deploy-master.ps1
#Requires -Version 5.1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
@ -26,72 +26,121 @@ function Write-Status {
Write-Host "$prefix $Message" -ForegroundColor $colors[$Type]
}
function Deploy-Project {
function Test-Environment {
$requiredVars = @{
"SPOTIFY_CLIENT_ID" = ""
"SPOTIFY_CLIENT_SECRET" = ""
"SPOTIFY_REDIRECT_URI" = ""
"CLOUDFLARE_API_TOKEN" = "For Cloudflare API access"
"CLOUDFLARE_ACCOUNT_ID" = "For Cloudflare account identification"
}
$missingVars = @()
foreach ($var in $requiredVars.GetEnumerator()) {
if (-not (Get-Item env:$($var.Key) -ErrorAction SilentlyContinue)) {
$message = "$($var.Key) is missing"
if ($var.Value) {
$message += ": $($var.Value)"
}
Write-Status $message "Warning"
$missingVars += $var.Key
}
}
# Check for deprecated variables
if (Get-Item env:CF_API_TOKEN -ErrorAction SilentlyContinue) {
Write-Status "CF_API_TOKEN is deprecated. Please use CLOUDFLARE_API_TOKEN instead" "Warning"
# Automatically migrate the value
$env:CLOUDFLARE_API_TOKEN = $env:CF_API_TOKEN
Remove-Item env:CF_API_TOKEN
}
if ($missingVars.Count -gt 0) {
throw "Missing required environment variables: $($missingVars -join ', ')"
}
}
function Clear-BuildArtifacts {
$paths = @("dist", ".wrangler", "node_modules/.cache")
foreach ($path in $paths) {
if (Test-Path $path) {
Remove-Item -Recurse -Force $path
Write-Status "Cleaned $path" "Success"
}
}
}
function Install-Dependencies {
Write-Status "Installing dependencies..." "Info"
npm ci --prefer-offline --no-audit
if ($LASTEXITCODE -ne 0) {
npm install
if ($LASTEXITCODE -ne 0) { throw "Failed to install dependencies" }
}
Write-Status "Dependencies installed successfully" "Success"
}
function Start-Build {
Write-Status "Building project..." "Info"
Write-Status "Building worker..." "Info"
npm run build:worker
if ($LASTEXITCODE -ne 0) { throw "Worker build failed" }
Write-Status "Building frontend..." "Info"
npm run build
if ($LASTEXITCODE -ne 0) { throw "Frontend build failed" }
if (-not (Test-Path "dist")) { throw "Build failed - dist directory not created" }
Write-Status "Build completed successfully" "Success"
}
function Deploy-Worker {
Write-Status "Deploying Cloudflare Worker..." "Info"
# Set wrangler environment variables
$env:WRANGLER_AUTH_TOKEN = $env:CLOUDFLARE_API_TOKEN
$deployEnv = if ($env:CI) { "production" } else { "development" }
npx wrangler deploy src/worker/index.ts --env $deployEnv
if ($LASTEXITCODE -ne 0) { throw "Worker deployment failed" }
Write-Status "Worker deployed successfully" "Success"
}
function Deploy-Frontend {
Write-Status "Deploying to Cloudflare Pages..." "Info"
npx wrangler pages deploy dist/
if ($LASTEXITCODE -ne 0) { throw "Pages deployment failed" }
Write-Status "Pages deployed successfully" "Success"
}
function Start-Deployment {
try {
# Create log directory if it doesn't exist
if (-not (Test-Path "logs")) { New-Item -ItemType Directory -Path "logs" }
# Start logging
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
if (-not (Test-Path "logs")) { New-Item -ItemType Directory -Path "logs" }
$logFile = "logs/deploy_$timestamp.log"
Start-Transcript -Path $logFile
# Check environment variables
Write-Status "Checking environment variables..." "Info"
$requiredVars = @(
"SPOTIFY_CLIENT_ID",
"SPOTIFY_CLIENT_SECRET",
"SPOTIFY_REDIRECT_URI"
)
$missingVars = @()
foreach ($var in $requiredVars) {
if (-not (Get-Item env:$var -ErrorAction SilentlyContinue)) {
$missingVars += $var
}
}
if ($missingVars.Count -gt 0) {
Write-Status "Missing environment variables: $($missingVars -join ', ')" "Error"
throw "Missing required environment variables"
}
# Clean and build
Write-Status "Cleaning previous builds..." "Info"
if (Test-Path "dist") { Remove-Item -Recurse -Force "dist" }
Write-Status "Installing dependencies..." "Info"
npm install
Write-Status "Building project..." "Info"
npm run build
if (-not (Test-Path "dist")) {
throw "Build failed - dist directory not created"
}
# Deploy Worker
Write-Status "Deploying Cloudflare Worker..." "Info"
$workerDeploy = npx wrangler deploy spotify-worker.ts 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Status "Worker deployment failed: $workerDeploy" "Error"
throw "Worker deployment failed"
}
Write-Status "Worker deployed successfully" "Success"
# Deploy Pages
Write-Status "Deploying to Cloudflare Pages..." "Info"
$pagesDeploy = npx wrangler pages deploy dist/ 2>&1
if ($LASTEXITCODE -ne 0) {
Write-Status "Pages deployment failed: $pagesDeploy" "Error"
throw "Pages deployment failed"
}
Write-Status "Pages deployed successfully" "Success"
$script:logFile = "logs/deploy_$timestamp.log"
Start-Transcript -Path $script:logFile
Test-Environment
Clear-BuildArtifacts
Install-Dependencies
Start-Build
Deploy-Worker
Deploy-Frontend
Write-Status "Deployment completed successfully!" "Success"
Write-Status "Log file: $logFile" "Info"
Write-Status "Log file: $script:logFile" "Info"
}
catch {
Write-Status "Deployment failed: $_" "Error"
Write-Status "Check the log file for details: $logFile" "Info"
Write-Status "Check the log file for details: $script:logFile" "Info"
exit 1
}
finally {
@ -99,5 +148,5 @@ function Deploy-Project {
}
}
# Execute deployment
Deploy-Project
# Start deployment
Start-Deployment

211
first-deploy.ps1 Normal file
View file

@ -0,0 +1,211 @@
# first-time-setup.ps1
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
function Write-Status {
param(
[string]$Message,
[string]$Type = "Info"
)
$colors = @{
Info = "Cyan"
Success = "Green"
Warning = "Yellow"
Error = "Red"
Input = "Magenta"
}
$prefix = switch ($Type) {
"Success" { "[+]" }
"Error" { "[-]" }
"Warning" { "[!]" }
"Info" { "[*]" }
"Input" { "[?]" }
}
Write-Host "$prefix $Message" -ForegroundColor $colors[$Type]
}
function Get-UserInput {
param(
[string]$Prompt,
[switch]$IsPassword
)
Write-Status $Prompt "Input"
if ($IsPassword) {
$secureString = Read-Host -AsSecureString
$BSTR = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($secureString)
$string = [System.Runtime.InteropServices.Marshal]::PtrToStringAuto($BSTR)
return $string
} else {
return Read-Host
}
}
function Open-BrowserIfConfirmed {
param([string]$Url, [string]$Message)
Write-Status $Message "Info"
$response = Get-UserInput "Would you like to open the browser now? (y/n)"
if ($response -eq 'y') {
Start-Process $Url
Write-Status "Browser opened. Press Enter once you've completed this step." "Input"
Read-Host | Out-Null
}
}
function Install-RequiredTools {
Write-Status "Checking required tools..." "Info"
# Check Node.js
if (-not (Get-Command "node" -ErrorAction SilentlyContinue)) {
Write-Status "Node.js is not installed. Please install it from: https://nodejs.org/" "Error"
Open-BrowserIfConfirmed "https://nodejs.org/" "Opening Node.js download page..."
throw "Node.js is required to continue"
}
# Check Git
if (-not (Get-Command "git" -ErrorAction SilentlyContinue)) {
Write-Status "Git is not installed. Please install it from: https://git-scm.com/" "Error"
Open-BrowserIfConfirmed "https://git-scm.com/downloads" "Opening Git download page..."
throw "Git is required to continue"
}
# Install wrangler globally
Write-Status "Installing Wrangler CLI..." "Info"
npm install -g wrangler
if ($LASTEXITCODE -ne 0) { throw "Failed to install Wrangler" }
}
function Setup-CloudflareAccount {
Write-Status "Setting up Cloudflare account..." "Info"
Write-Status "If you don't have a Cloudflare account, you'll need to create one." "Info"
Open-BrowserIfConfirmed "https://dash.cloudflare.com/sign-up" "Opening Cloudflare signup page..."
# Login to Cloudflare using Wrangler
Write-Status "Logging in to Cloudflare..." "Info"
npx wrangler login
if ($LASTEXITCODE -ne 0) { throw "Failed to login to Cloudflare" }
# Get Account ID
Write-Status "Please get your Cloudflare Account ID from the Cloudflare Dashboard." "Info"
Write-Status "You can find it at https://dash.cloudflare.com/ in the right sidebar." "Info"
Open-BrowserIfConfirmed "https://dash.cloudflare.com/" "Opening Cloudflare Dashboard..."
$accountId = Get-UserInput "Enter your Cloudflare Account ID"
$apiToken = Get-UserInput "Enter your Cloudflare API Token (from https://dash.cloudflare.com/profile/api-tokens)" -IsPassword
# Create .env.local file
Write-Status "Creating .env.local file..." "Info"
$envContent = @"
CLOUDFLARE_API_TOKEN=$apiToken
CLOUDFLARE_ACCOUNT_ID=$accountId
SPOTIFY_CLIENT_ID=
SPOTIFY_CLIENT_SECRET=
SPOTIFY_REDIRECT_URI=
"@
Set-Content -Path ".env.local" -Value $envContent
Write-Status "Created .env.local file with Cloudflare credentials" "Success"
}
function Setup-SpotifyApp {
Write-Status "Setting up Spotify Developer App..." "Info"
Open-BrowserIfConfirmed "https://developer.spotify.com/dashboard" "Opening Spotify Developer Dashboard..."
Write-Status "Create a new app in the Spotify Developer Dashboard" "Info"
Write-Status "Once created, get the Client ID and Client Secret" "Info"
$clientId = Get-UserInput "Enter your Spotify Client ID"
$clientSecret = Get-UserInput "Enter your Spotify Client Secret" -IsPassword
$redirectUri = "https://personal-site.pages.dev/callback"
# Update .env.local file
$envContent = Get-Content ".env.local" -Raw
$envContent = $envContent.Replace("SPOTIFY_CLIENT_ID=", "SPOTIFY_CLIENT_ID=$clientId")
$envContent = $envContent.Replace("SPOTIFY_CLIENT_SECRET=", "SPOTIFY_CLIENT_SECRET=$clientSecret")
$envContent = $envContent.Replace("SPOTIFY_REDIRECT_URI=", "SPOTIFY_REDIRECT_URI=$redirectUri")
Set-Content -Path ".env.local" -Value $envContent
Write-Status "Updated .env.local file with Spotify credentials" "Success"
}
function Setup-Project {
Write-Status "Setting up project..." "Info"
# Install dependencies
Write-Status "Installing project dependencies..." "Info"
npm install
if ($LASTEXITCODE -ne 0) { throw "Failed to install dependencies" }
# Initialize git if not already initialized
if (-not (Test-Path ".git")) {
Write-Status "Initializing git repository..." "Info"
git init
git add .
git commit -m "Initial commit"
}
# Create initial wrangler.toml if it doesn't exist
if (-not (Test-Path "wrangler.toml")) {
Write-Status "Creating wrangler.toml..." "Info"
$wranglerContent = @"
name = "personal-site"
main = "src/worker/index.ts"
compatibility_date = "2024-02-01"
[build]
command = "npm run build:worker"
[env.production]
name = "personal-site"
vars = { ENVIRONMENT = "production" }
[env.development]
name = "personal-site-dev"
vars = { ENVIRONMENT = "development" }
[dev]
port = 8787
"@
Set-Content -Path "wrangler.toml" -Value $wranglerContent
}
}
function Start-FirstTimeSetup {
try {
Write-Status "Starting first-time setup..." "Info"
# Create log directory if it doesn't exist
if (-not (Test-Path "logs")) {
New-Item -ItemType Directory -Path "logs" | Out-Null
}
# Start logging
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$logFile = "logs/setup_$timestamp.log"
Start-Transcript -Path $logFile
# Run setup steps
Install-RequiredTools
Setup-CloudflareAccount
Setup-SpotifyApp
Setup-Project
Write-Status "First-time setup completed successfully!" "Success"
Write-Status "You can now run './deploy-master.ps1' to deploy your site" "Info"
Write-Status "Log file: $logFile" "Info"
}
catch {
Write-Status "Setup failed: $_" "Error"
Write-Status "Check the log file for details: $logFile" "Info"
exit 1
}
finally {
Stop-Transcript
}
}
# Start setup
Start-FirstTimeSetup

10196
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,96 +1,97 @@
{
"name": "personal-site",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "powershell ./dev.ps1",
"dev": "vite",
"build": "tsc \u0026\u0026 vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"deploy": "powershell ./deploy.ps1",
"setup": "powershell ./first-time-setup.ps1",
"clean": "rimraf node_modules dist .wrangler .cloudflare",
"type-check": "tsc --noEmit",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"test": "vitest",
"test:ui": "vitest --ui",
"prepare": "husky install",
"build:worker": "esbuild --bundle src/worker/index.ts --outfile=dist/worker/index.js --format=esm --platform=browser",
"dev:worker": "wrangler dev src/worker/index.ts",
"deploy:worker": "wrangler deploy src/worker/index.ts"
},
"dependencies": {
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"itty-router": "^4.0.27",
"lucide-react": "^0.330.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.0",
"tailwind-merge": "^2.2.1"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240208.0",
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20.11.17",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/ui": "^1.2.2",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"husky": "^9.0.10",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"rimraf": "^5.0.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.0",
"vitest": "^1.2.2",
"wrangler": "^3.28.0"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{html,css,json,md}": [
"prettier --write"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"engines": {
"node": "\u003e=18.0.0"
},
"browserslist": {
"production": [
"\u003e0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
"name": "personal-site",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"start": "powershell ./dev.ps1",
"dev": "vite",
"build": "tsc && vite build",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview",
"deploy": "powershell ./deploy.ps1",
"setup": "powershell ./first-time-setup.ps1",
"clean": "rimraf node_modules dist .wrangler .cloudflare",
"type-check": "tsc --noEmit",
"format": "prettier --write \"src/**/*.{ts,tsx,css}\"",
"test": "vitest",
"test:ui": "vitest --ui",
"prepare": "husky install",
"build:worker": "esbuild --bundle src/worker/index.ts --outfile=dist/worker/index.js --format=esm --platform=browser",
"dev:worker": "wrangler dev src/worker/index.ts",
"deploy:worker": "wrangler deploy src/worker/index.ts"
},
"dependencies": {
"@radix-ui/react-slot": "^1.0.2",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.0",
"itty-router": "^4.0.27",
"lucide-react": "^0.330.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.22.0",
"tailwind-merge": "^2.2.1"
},
"devDependencies": {
"@cloudflare/workers-types": "4.20250129.0",
"@commitlint/cli": "^18.6.0",
"@commitlint/config-conventional": "^18.6.0",
"@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1",
"@testing-library/user-event": "^14.5.2",
"@types/node": "^20.11.17",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitejs/plugin-react-swc": "^3.5.0",
"@vitest/ui": "^1.2.2",
"autoprefixer": "^10.4.17",
"eslint": "^8.56.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5",
"husky": "^9.0.10",
"jsdom": "^24.0.0",
"lint-staged": "^15.2.2",
"postcss": "^8.4.35",
"prettier": "^3.2.5",
"prettier-plugin-tailwindcss": "^0.5.11",
"rimraf": "^5.0.5",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^5.1.0",
"vitest": "^1.2.2",
"web-vitals": "4.2.4",
"wrangler": "^3.28.0"
},
"lint-staged": {
"*.{ts,tsx}": [
"eslint --fix",
"prettier --write"
],
"*.{html,css,json,md}": [
"prettier --write"
]
},
"commitlint": {
"extends": [
"@commitlint/config-conventional"
]
},
"engines": {
"node": ">=18.0.0"
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
}
}

View file

@ -1,11 +1,10 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from '@/components/Navbar';
import AboutPage from '@/pages/AboutPage';
import APCSPPage from '@/pages/APCSPPage';
import ProjectsPage from '@/pages/ProjectsPage';
import ErrorBoundary from '@/components/ErrorBoundary';
import '@/styles/.css';
import '@/styles/App.css';
const App = () => {
return (

View file

@ -1,28 +1,13 @@
import { useEffect, useState } from 'react';
import { GithubRepo } from '@/types';
import { GithubRepo } from '@/types';
import '@/styles/GithubRepos.css';
const GithubRepos = () => {
const [repos, setRepos] = useState<GithubRepo[]>([]);
useEffect(() => {
const fetchRepos = async () => {
try {
const response = await fetch('https://api.github.com/users/EndofTimee/repos');
if (!response.ok) throw new Error('Failed to fetch repositories');
const data: GithubRepo[] = await response.json();
setRepos(data);
} catch (error) {
console.error('Error fetching GitHub repos:', error);
}
};
fetchRepos();
}, []);
interface GithubReposProps {
repos: GithubRepo[];
}
const GithubRepos: React.FC<GithubReposProps> = ({ repos }) => {
return (
<div className="github-repos-container">
<h1>My GitHub Repositories</h1>
<div className="repos-grid">
{repos.map((repo) => (
<div key={repo.id} className="repo-card">
@ -35,6 +20,15 @@ const GithubRepos = () => {
{repo.language && (
<span className="repo-language">{repo.language}</span>
)}
{repo.languages && repo.languages.length > 0 && (
<div className="repo-languages">
{repo.languages.map((lang) => (
<span key={lang} className="language-tag">
{lang}
</span>
))}
</div>
)}
</div>
))}
</div>

View file

@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import '@/styles/.css';
import { useEffect, useState } from 'react';
import '@/styles/LoadingAnimation.css';
const generateRandomCode = () => {
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+[]{}|;:,.<>?';
@ -7,7 +7,7 @@ const generateRandomCode = () => {
};
const LoadingAnimation = () => {
const [codeLines, setCodeLines] = useState([]);
const [codeLines, setCodeLines] = useState<string[]>([]);
useEffect(() => {
setCodeLines(generateRandomCode());

View file

@ -1,6 +1,4 @@
import React from 'react';
import '@/styles/.css';
import '@/styles/parallax-effect.css';
const ParallaxEffect = () => {
return (
@ -18,4 +16,3 @@ const ParallaxEffect = () => {
};
export default ParallaxEffect;

View file

@ -1,81 +1,85 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import type { SpotifyTrack } from '@/types';
const WORKER_URL = 'https://spotify-worker.your-worker-subdomain.workers.dev';
interface SpotifyResponse {
items: SpotifyTrack[];
}
function SpotifyList() {
const [tracks, setTracks] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [tracks, setTracks] = useState<SpotifyTrack[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchTracks = async () => {
try {
setLoading(true);
const response = await fetch(`${WORKER_URL}/top-tracks`);
if (!response.ok) {
throw new Error('Failed to fetch tracks');
}
useEffect(() => {
const fetchTracks = async () => {
try {
setLoading(true);
const response = await fetch(`${import.meta.env.VITE_WORKER_URL}/top-tracks`);
if (!response.ok) {
throw new Error('Failed to fetch tracks');
}
const data = await response.json();
setTracks(data.items || []);
} catch (err) {
setError(err.message);
console.error('Error fetching tracks:', err);
} finally {
setLoading(false);
}
};
const data = await response.json() as SpotifyResponse;
setTracks(data.items || []);
} catch (error) {
const err = error as Error;
setError(err.message);
console.error('Error fetching tracks:', error);
} finally {
setLoading(false);
}
};
fetchTracks();
}, []);
fetchTracks();
}, []);
if (loading) {
return (
<Card className="w-full max-w-md mx-auto">
<CardContent className="p-6">
<div className="flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="w-full max-w-md mx-auto border-red-200">
<CardContent className="p-6">
<p className="text-red-500">Error: {error}</p>
</CardContent>
</Card>
);
}
if (loading) {
return (
<Card className="w-full max-w-md mx-auto">
<CardContent className="p-6">
<div className="flex justify-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-gray-900"></div>
</div>
</CardContent>
</Card>
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>My Top Tracks</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{tracks.map((track, index) => (
<li
key={track.id}
className="p-2 hover:bg-gray-50 rounded-lg transition-colors"
>
<span className="font-medium">{index + 1}.</span>{' '}
{track.name} by{' '}
<span className="text-gray-600">
{track.artists.map(artist => artist.name).join(', ')}
</span>
</li>
))}
</ul>
</CardContent>
</Card>
);
}
if (error) {
return (
<Card className="w-full max-w-md mx-auto border-red-200">
<CardContent className="p-6">
<p className="text-red-500">Error: {error}</p>
</CardContent>
</Card>
);
}
return (
<Card className="w-full max-w-md mx-auto">
<CardHeader>
<CardTitle>My Top Tracks</CardTitle>
</CardHeader>
<CardContent>
<ul className="space-y-2">
{tracks.map((track, index) => (
<li
key={track.id}
className="p-2 hover:bg-gray-50 rounded-lg transition-colors"
>
<span className="font-medium">{index + 1}.</span>{' '}
{track.name} by{' '}
<span className="text-gray-600">
{track.artists.map(artist => artist.name).join(', ')}
</span>
</li>
))}
</ul>
</CardContent>
</Card>
);
}
export default SpotifyList;

View file

@ -0,0 +1,17 @@
import { type CardProps } from '@/types';
export const Card: React.FC<CardProps> = ({ className, children }) => (
<div className={`card ${className || ''}`}>{children}</div>
);
export const CardHeader: React.FC<CardProps> = ({ className, children }) => (
<div className={`card-header ${className || ''}`}>{children}</div>
);
export const CardTitle: React.FC<CardProps> = ({ className, children }) => (
<h2 className={`card-title ${className || ''}`}>{children}</h2>
);
export const CardContent: React.FC<CardProps> = ({ className, children }) => (
<div className={`card-content ${className || ''}`}>{children}</div>
);

View file

@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import { GithubRepo } from '@/types';
import { useState, useEffect } from 'react';
import type { GithubRepo } from '@/types';
const useGithubRepos = () => {
const [repos, setRepos] = useState<GithubRepo[]>([]);
@ -13,13 +13,13 @@ const useGithubRepos = () => {
if (!response.ok) {
throw new Error('Failed to fetch repositories');
}
const data = await response.json();
const reposData = await response.json() as GithubRepo[];
const repoDetails = await Promise.all(
data.map(async (repo: GithubRepo) => {
reposData.map(async (repo: GithubRepo) => {
try {
const languagesResponse = await fetch(repo.languages_url);
const languages = await languagesResponse.json();
const languages = await languagesResponse.json() as Record<string, number>;
return {
...repo,
languages: Object.keys(languages)
@ -37,7 +37,8 @@ const useGithubRepos = () => {
setRepos(repoDetails);
setError(null);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
const error = err as Error;
setError(error.message);
console.error('Error fetching repos:', err);
} finally {
setLoading(false);

View file

@ -1,13 +1,11 @@
import { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import type { SpotifyTrack } from '@/types';
interface SpotifyData {
data: SpotifyTrack[] | null;
loading: boolean;
error: string | null;
interface SpotifyResponse {
items: SpotifyTrack[];
}
const useSpotifyData = (): SpotifyData => {
const useSpotifyData = () => {
const [data, setData] = useState<SpotifyTrack[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
@ -21,10 +19,11 @@ const useSpotifyData = (): SpotifyData => {
const response = await fetch(`${workerUrl}/spotify-data`);
if (!response.ok) throw new Error('Failed to fetch Spotify data');
const result = await response.json();
setData(result);
const result = await response.json() as SpotifyResponse;
setData(result.items);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
const error = err as Error;
setError(error.message);
} finally {
setLoading(false);
}

View file

@ -1,6 +1,5 @@
import React from 'react';
import FoxCard from '../components/FoxCard';
import { Code, BookOpen, Cpu } from 'lucide-react';
import FoxCard from '@/components/FoxCard';
import { Code, Cpu } from 'lucide-react';
const APCSPPage = () => {
return (
@ -40,4 +39,3 @@ const APCSPPage = () => {
};
export default APCSPPage;

View file

@ -1,4 +1,4 @@
import { Heart, Gamepad2, Code, Music } from 'lucide-react';
import { Gamepad2, Code, Music } from 'lucide-react';
import FoxCard from '@/components/FoxCard';
import SpotifyVisualizer from '@/components/SpotifyVisualizer';
import useSpotifyData from '@/hooks/useSpotifyData';
@ -70,4 +70,4 @@ const AboutPage = () => {
);
};
export default AboutPage;
export default AboutPage;

View file

@ -1,8 +1,7 @@
import React from 'react';
import FoxCard from '../components/FoxCard';
import GithubRepos from '../components/GithubRepos';
import useGithubRepos from '../hooks/useGithubRepos';
import LoadingFox from '../components/LoadingFox';
import FoxCard from '@/components/FoxCard';
import GithubRepos from '@/components/GithubRepos';
import useGithubRepos from '@/hooks/useGithubRepos';
import LoadingFox from '@/components/LoadingFox';
const ProjectsPage = () => {
const { repos, loading, error } = useGithubRepos();
@ -31,4 +30,3 @@ const ProjectsPage = () => {
};
export default ProjectsPage;

View file

@ -1,14 +1,16 @@
const reportWebVitals = onPerfEntry => {
if (onPerfEntry && onPerfEntry instanceof Function) {
import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
getCLS(onPerfEntry);
getFID(onPerfEntry);
getFCP(onPerfEntry);
getLCP(onPerfEntry);
getTTFB(onPerfEntry);
});
}
const reportWebVitals = (onPerfEntry?: (metric: any) => void): void => {
if (onPerfEntry && typeof onPerfEntry === 'function') {
import('web-vitals').then((vitals) => {
const { onCLS, onFID, onFCP, onLCP, onTTFB } = vitals;
onCLS(onPerfEntry);
onFID(onPerfEntry);
onFCP(onPerfEntry);
onLCP(onPerfEntry);
onTTFB(onPerfEntry);
}).catch((error) => {
console.error('Error loading web-vitals:', error);
});
}
};
export default reportWebVitals;

View file

@ -1,54 +1,69 @@
.github-repos-container {
padding: 2rem;
background-color: #f9f9f9;
font-family: Arial, sans-serif;
text-align: center;
.github-repos-container {
width: 100%;
padding: 1rem;
}
.repos-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 1.5rem;
margin-top: 2rem;
}
.repo-card {
background: #fff;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
padding: 1rem;
transition: transform 0.2s ease, box-shadow 0.2s ease;
background: rgba(47, 28, 84, 0.3);
border: 1px solid rgba(157, 78, 221, 0.2);
border-radius: 12px;
padding: 1.5rem;
transition: all 0.3s ease;
}
.repo-card:hover {
transform: translateY(-5px);
box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15);
border-color: var(--accent-neon);
box-shadow: 0 0 20px rgba(178, 73, 248, 0.2);
}
.repo-name {
font-size: 1.2rem;
font-weight: bold;
color: #0077cc;
font-weight: 600;
color: var(--text-primary);
text-decoration: none;
margin-bottom: 0.5rem;
display: block;
}
.repo-name:hover {
text-decoration: underline;
color: var(--accent-neon);
}
.repo-description {
color: #555;
margin-top: 0.5rem;
color: var(--text-primary);
opacity: 0.8;
margin: 0.5rem 0;
font-size: 0.9rem;
}
.repo-language {
display: inline-block;
margin-top: 1rem;
padding: 0.3rem 0.6rem;
background-color: #eee;
border-radius: 4px;
font-size: 0.9rem;
color: #333;
padding: 0.25rem 0.75rem;
background: rgba(157, 78, 221, 0.2);
border-radius: 1rem;
font-size: 0.8rem;
color: var(--text-primary);
margin-top: 0.5rem;
}
.repo-languages {
margin-top: 0.5rem;
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.language-tag {
padding: 0.25rem 0.75rem;
background: rgba(157, 78, 221, 0.1);
border-radius: 1rem;
font-size: 0.8rem;
color: var(--text-primary);
}

View file

@ -1,3 +1,3 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 8C16 8 18 12 22 14C26 16 28 16 28 16C28 16 26 20 22 22C18 24 16 24 16 24C16 24 14 20 10 18C6 16 4 16 4 16C4 16 6 12 10 10C14 8 16 8 16 8Z" fill="#9d4edd" stroke="#ffc6e5" stroke-width="1.5"/>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#ffffff" stroke="#000000" stroke-width="2" d="M6,2 L26,22 L16,22 L12,30 L8,22 L6,22 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 313 B

After

Width:  |  Height:  |  Size: 196 B

View file

@ -1,4 +1,5 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M12 8C12 10.2091 10.2091 12 8 12C5.79086 12 4 10.2091 4 8C4 5.79086 5.79086 4 8 4C10.2091 4 12 5.79086 12 8Z" fill="#9d4edd"/>
<path d="M28 8C28 10.2091 26.2091 12 24 12C21.7909 12 20 10.2091 20 8C20 5.79086 21.7909 4 24 4C26.2091 4 28 5.79086 28 8Z" fill="#9d4edd"/>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="#ffc6e5" stroke="#ff9466" stroke-width="2" d="M12,4 C14,4 15,6 15,8 C15,10 14,12 12,12 C10,12 9,10 9,8 C9,6 10,4 12,4 Z"/>
<path fill="#ffc6e5" stroke="#ff9466" stroke-width="2" d="M20,4 C22,4 23,6 23,8 C23,10 22,12 20,12 C18,12 17,10 17,8 C17,6 18,4 20,4 Z"/>
<path fill="#ffc6e5" stroke="#ff9466" stroke-width="2" d="M16,8 C20,8 23,12 23,16 C23,20 20,24 16,24 C12,24 9,20 9,16 C9,12 12,8 16,8 Z"/>
</svg>

Before

Width:  |  Height:  |  Size: 387 B

After

Width:  |  Height:  |  Size: 515 B

View file

@ -1,12 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4C16 4 24 8 24 16C24 24 16 28 16 28" stroke="#9d4edd" stroke-width="2">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 16 16"
to="360 16 16"
dur="1s"
repeatCount="indefinite"/>
</path>
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32">
<path fill="none" stroke="#ffc6e5" stroke-width="2">
<animateTransform
attributeName="transform"
type="rotate"
from="0 16 16"
to="360 16 16"
dur="1s"
repeatCount="indefinite"/>
<path d="M16,4 C22,4 26,8 26,14 C26,20 22,24 16,24 C10,24 6,20 6,14 C6,8 10,4 16,4 Z"/>
</path>
</svg>

Before

Width:  |  Height:  |  Size: 393 B

After

Width:  |  Height:  |  Size: 460 B

View file

@ -1,13 +1,11 @@
// src/types/index.ts
import { ReactNode } from 'react';
import { ReactNode } from 'react';
export interface SpotifyTrack {
export interface Track {
id: string;
name: string;
artists: {
artists: Array<{
name: string;
}[];
}>;
}
export interface GithubRepo {
@ -20,26 +18,48 @@ export interface GithubRepo {
languages: string[];
}
export interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
export interface SpotifyTokenResponse {
access_token: string;
token_type: string;
expires_in: number;
}
export interface SpotifyError {
error: {
status: number;
message: string;
};
}
export interface CardProps {
className?: string;
children: ReactNode;
}
export interface FoxCardProps {
className?: string;
children: ReactNode;
}
export interface ErrorBoundaryProps {
children: ReactNode;
}
export interface FoxCardProps {
children: ReactNode;
className?: string;
export interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
export interface SpotifyVisualizerProps {
isPlaying?: boolean;
}
export interface ThemeColors {
primary: string;
secondary: string;
accent: string;
export interface SpotifyTrack {
id: string;
name: string;
artists: Array<{ name: string }>;
}
export interface GithubReposProps {
repos: GithubRepo[];
}

View file

@ -0,0 +1,15 @@
// src/worker/routes/health.ts
import { Router } from 'itty-router';
import { jsonResponse } from '../utils/response';
import type { HealthCheckResponse } from '../types';
export const healthRouter = Router({ base: '/health' });
healthRouter.get('/', () => {
const healthCheck: HealthCheckResponse = {
status: 'healthy',
timestamp: new Date().toISOString()
};
return jsonResponse(healthCheck);
});

View file

@ -0,0 +1,29 @@
import { Router } from 'itty-router';
import { jsonResponse, errorResponse } from '../utils/response';
import { getSpotifyToken } from '../utils/spotify';
import type { Env } from '../types';
import type { SpotifyTopTracksResponse } from '../types/spotify';
export const spotifyRouter = Router({ base: '/spotify' });
spotifyRouter.get('/top-tracks', async ({ env }: { env: Env }) => {
try {
const accessToken = await getSpotifyToken(env);
const response = await fetch('https://api.spotify.com/v1/me/top/tracks?limit=10', {
headers: {
'Authorization': `Bearer ${accessToken}`
}
});
if (!response.ok) {
throw new Error(`Failed to fetch tracks: ${response.status}`);
}
const data = await response.json() as SpotifyTopTracksResponse;
return jsonResponse(data);
} catch (error) {
const err = error as Error;
return errorResponse(err.message);
}
});

View file

@ -1,7 +1,5 @@
// src/worker/utils/spotify.ts
import type { SpotifyTokenResponse, SpotifyError } from '../types/spotify';
import type { Env } from '../types';
import type { SpotifyTokenResponse, SpotifyError } from '../types/spotify';
import { errorResponse } from './response';
export async function getSpotifyToken(env: Env): Promise<string> {
try {
@ -18,7 +16,7 @@ export async function getSpotifyToken(env: Env): Promise<string> {
throw new Error(`Failed to get token: ${response.status}`);
}
const data = await response.json<SpotifyTokenResponse | SpotifyError>();
const data = await response.json() as SpotifyTokenResponse | SpotifyError;
if ('error' in data) {
throw new Error(data.error.message);
@ -26,6 +24,7 @@ export async function getSpotifyToken(env: Env): Promise<string> {
return data.access_token;
} catch (error) {
throw new Error(`Failed to get Spotify token: ${error instanceof Error ? error.message : 'Unknown error'}`);
const err = error as Error;
throw new Error(`Failed to get Spotify token: ${err.message}`);
}
}

View file

@ -1,25 +1,26 @@
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["./src/*"]
},
"types": ["@cloudflare/workers-types"]
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}