mirror of
https://github.com/System-End/My-website.git
synced 2026-04-19 22:05:07 +00:00
.
This commit is contained in:
parent
fd6e3b911d
commit
8628244de9
25 changed files with 10925 additions and 379 deletions
|
|
@ -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
211
first-deploy.ps1
Normal 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
10196
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
189
package.json
189
package.json
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
17
src/components/ui/card.tsx
Normal file
17
src/components/ui/card.tsx
Normal 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>
|
||||
);
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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 |
|
|
@ -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[];
|
||||
}
|
||||
|
|
|
|||
15
src/worker/routes/health.ts
Normal file
15
src/worker/routes/health.ts
Normal 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);
|
||||
});
|
||||
29
src/worker/routes/spotify.ts
Normal file
29
src/worker/routes/spotify.ts
Normal 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);
|
||||
}
|
||||
});
|
||||
|
|
@ -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}`);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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" }]
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue