From 41b31ad4c85494e922b0c106999c88ec63c26a4e Mon Sep 17 00:00:00 2001 From: Unknown <53575465+EndofTimee@users.noreply.github.com> Date: Sat, 1 Feb 2025 08:39:04 -0700 Subject: [PATCH] . --- package.json | 187 ++++++++++++++------------- src/components/ErrorBoundary.tsx | 12 +- src/components/FoxCard.tsx | 21 ++- src/components/GithubRepos.tsx | 12 +- src/components/LoadingFox.tsx | 4 +- src/components/Navbar.tsx | 4 - src/components/SpotifyVisualizer.tsx | 24 ++-- src/components/ThemeToggle.tsx | 39 +++--- src/hooks/useGithubRepos.tsx | 12 +- src/hooks/useSpotifyData.tsx | 23 +++- src/pages/AboutPage.tsx | 30 +++-- src/styles/animations.css | 31 +++++ src/styles/base.css | 49 +++++++ src/styles/cursor.css | 14 +- src/styles/index.css | 17 +-- src/styles/utilities.css | 27 ++++ src/types/index.ts | 37 ++++-- src/worker/index.ts | 24 ++++ src/worker/types/index.ts | 18 +++ src/worker/types/spotify.ts | 42 ++++++ src/worker/utils/response.ts | 24 ++++ src/worker/utils/spotify.ts | 31 +++++ tailwind.config.js | 26 +++- tsconfig.worker.json | 26 ++++ wrangler.toml | 13 +- 25 files changed, 536 insertions(+), 211 deletions(-) create mode 100644 src/styles/animations.css create mode 100644 src/styles/base.css create mode 100644 src/styles/utilities.css create mode 100644 src/worker/index.ts create mode 100644 src/worker/types/index.ts create mode 100644 src/worker/types/spotify.ts create mode 100644 src/worker/utils/response.ts create mode 100644 src/worker/utils/spotify.ts create mode 100644 tsconfig.worker.json diff --git a/package.json b/package.json index 2d2fa1e..b657a62 100644 --- a/package.json +++ b/package.json @@ -1,93 +1,96 @@ { - "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" - }, - "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": ">=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" - ] - } - } \ No newline at end of file + "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" + ] + } +} diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 9b23b99..79fe47d 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,16 +1,17 @@ -import React from 'react'; +import { Component, ErrorInfo } from 'react'; +import { ErrorBoundaryProps, ErrorBoundaryState } from '@/types'; -class ErrorBoundary extends React.Component { - constructor(props) { +class ErrorBoundary extends Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false, error: null }; } - static getDerivedStateFromError(error) { + static getDerivedStateFromError(error: Error): ErrorBoundaryState { return { hasError: true, error }; } - componentDidCatch(error, errorInfo) { + componentDidCatch(error: Error, errorInfo: ErrorInfo): void { console.error('Error caught by boundary:', error, errorInfo); } @@ -44,4 +45,3 @@ class ErrorBoundary extends React.Component { } export default ErrorBoundary; - diff --git a/src/components/FoxCard.tsx b/src/components/FoxCard.tsx index 228ac27..75806b3 100644 --- a/src/components/FoxCard.tsx +++ b/src/components/FoxCard.tsx @@ -1,15 +1,12 @@ -import React from 'react'; -import '../styles/FoxCard.css'; +import type { FoxCardProps } from '@/types'; +import '@/styles/FoxCard.css'; -const FoxCard = ({ children, className = '' }) => { - return ( -
-
-
- {children} -
- ); -}; +const FoxCard = ({ children, className = '' }: FoxCardProps) => ( +
+
+
+ {children} +
+); export default FoxCard; - diff --git a/src/components/GithubRepos.tsx b/src/components/GithubRepos.tsx index 83190a8..d33697d 100644 --- a/src/components/GithubRepos.tsx +++ b/src/components/GithubRepos.tsx @@ -1,15 +1,16 @@ - -import React, { useEffect, useState } from 'react'; -import '@/styles/.css'; +import { useEffect, useState } from 'react'; +import { GithubRepo } from '@/types'; +import '@/styles/GithubRepos.css'; const GithubRepos = () => { - const [repos, setRepos] = useState([]); + const [repos, setRepos] = useState([]); useEffect(() => { const fetchRepos = async () => { try { const response = await fetch('https://api.github.com/users/EndofTimee/repos'); - const data = await response.json(); + 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); @@ -42,4 +43,3 @@ const GithubRepos = () => { }; export default GithubRepos; - diff --git a/src/components/LoadingFox.tsx b/src/components/LoadingFox.tsx index dc0ac1f..845f62c 100644 --- a/src/components/LoadingFox.tsx +++ b/src/components/LoadingFox.tsx @@ -1,5 +1,4 @@ -import React from 'react'; -import '../styles/LoadingFox.css'; +import '@/styles/LoadingFox.css'; const LoadingFox = () => { return ( @@ -23,4 +22,3 @@ const LoadingFox = () => { }; export default LoadingFox; - diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index b6e9657..aabaea7 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -1,6 +1,4 @@ -import React from 'react'; import { Link, useLocation } from 'react-router-dom'; -import ThemeToggle from './ThemeToggle'; import { Home, Code, BookOpen, Twitch } from 'lucide-react'; const Navbar = () => { @@ -49,8 +47,6 @@ const Navbar = () => { Stream
- -
); diff --git a/src/components/SpotifyVisualizer.tsx b/src/components/SpotifyVisualizer.tsx index f875933..fad283b 100644 --- a/src/components/SpotifyVisualizer.tsx +++ b/src/components/SpotifyVisualizer.tsx @@ -1,17 +1,24 @@ -import React, { useEffect, useRef } from 'react'; -import '../styles/SpotifyVisualizer.css'; +import { useEffect, useRef } from 'react'; +import type { SpotifyVisualizerProps } from '@/types'; +import '@/styles/SpotifyVisualizer.css'; -const SpotifyVisualizer = ({ isPlaying }) => { - const canvasRef = useRef(null); - const animationRef = useRef(null); +const SpotifyVisualizer = ({ isPlaying = false }: SpotifyVisualizerProps) => { + const canvasRef = useRef(null); + const animationRef = useRef(null); useEffect(() => { const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext('2d'); + if (!ctx) return; + const bars = 50; const barWidth = canvas.width / bars; const animate = () => { + if (!canvas || !ctx) return; + ctx.clearRect(0, 0, canvas.width, canvas.height); for (let i = 0; i < bars; i++) { @@ -47,9 +54,9 @@ const SpotifyVisualizer = ({ isPlaying }) => { return (
@@ -57,4 +64,3 @@ const SpotifyVisualizer = ({ isPlaying }) => { }; export default SpotifyVisualizer; - diff --git a/src/components/ThemeToggle.tsx b/src/components/ThemeToggle.tsx index 86b6a9f..cb5a203 100644 --- a/src/components/ThemeToggle.tsx +++ b/src/components/ThemeToggle.tsx @@ -1,20 +1,29 @@ -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; -function ThemeToggle() { - useEffect(() => { - const colorPicker = document.getElementById('theme-color-picker'); - const currentColor = localStorage.getItem('theme-color') || '#3f10ad'; - document.documentElement.style.setProperty('--primary-color', currentColor); - colorPicker.value = currentColor; +const ThemeToggle = () => { + useEffect(() => { + const colorPicker = document.getElementById('theme-color-picker') as HTMLInputElement; + if (!colorPicker) return; - colorPicker.addEventListener('input', (event) => { - const newColor = event.target.value; - document.documentElement.style.setProperty('--primary-color', newColor); - localStorage.setItem('theme-color', newColor); - }); - }, []); + const currentColor = localStorage.getItem('theme-color') || '#3f10ad'; + document.documentElement.style.setProperty('--primary-color', currentColor); + colorPicker.value = currentColor; - return null; -} + const handleInput = (event: Event) => { + const input = event.target as HTMLInputElement; + const newColor = input.value; + document.documentElement.style.setProperty('--primary-color', newColor); + localStorage.setItem('theme-color', newColor); + }; + + colorPicker.addEventListener('input', handleInput); + + return () => { + colorPicker.removeEventListener('input', handleInput); + }; + }, []); + + return null; +}; export default ThemeToggle; diff --git a/src/hooks/useGithubRepos.tsx b/src/hooks/useGithubRepos.tsx index 6fc905c..df3cec4 100644 --- a/src/hooks/useGithubRepos.tsx +++ b/src/hooks/useGithubRepos.tsx @@ -1,9 +1,10 @@ import { useState, useEffect } from 'react'; +import { GithubRepo } from '@/types'; const useGithubRepos = () => { - const [repos, setRepos] = useState([]); + const [repos, setRepos] = useState([]); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); useEffect(() => { const fetchRepos = async () => { @@ -14,9 +15,8 @@ const useGithubRepos = () => { } const data = await response.json(); - // Get additional details for each repo const repoDetails = await Promise.all( - data.map(async (repo) => { + data.map(async (repo: GithubRepo) => { try { const languagesResponse = await fetch(repo.languages_url); const languages = await languagesResponse.json(); @@ -35,8 +35,9 @@ const useGithubRepos = () => { ); setRepos(repoDetails); + setError(null); } catch (err) { - setError(err.message); + setError(err instanceof Error ? err.message : 'An error occurred'); console.error('Error fetching repos:', err); } finally { setLoading(false); @@ -50,4 +51,3 @@ const useGithubRepos = () => { }; export default useGithubRepos; - diff --git a/src/hooks/useSpotifyData.tsx b/src/hooks/useSpotifyData.tsx index 6654c32..5bfa610 100644 --- a/src/hooks/useSpotifyData.tsx +++ b/src/hooks/useSpotifyData.tsx @@ -1,21 +1,31 @@ import { useState, useEffect } from 'react'; +import type { SpotifyTrack } from '@/types'; -const useSpotifyData = () => { - const [data, setData] = useState(null); +interface SpotifyData { + data: SpotifyTrack[] | null; + loading: boolean; + error: string | null; +} + +const useSpotifyData = (): SpotifyData => { + const [data, setData] = useState(null); const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { - const response = await fetch(`${process.env.REACT_APP_WORKER_URL}/spotify-data`); + const workerUrl = import.meta.env.VITE_WORKER_URL; + if (!workerUrl) throw new Error('Worker URL not configured'); + + 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); - setLoading(false); } catch (err) { - setError(err.message); + setError(err instanceof Error ? err.message : 'An error occurred'); + } finally { setLoading(false); } }; @@ -27,4 +37,3 @@ const useSpotifyData = () => { }; export default useSpotifyData; - diff --git a/src/pages/AboutPage.tsx b/src/pages/AboutPage.tsx index b2dda5e..18a1291 100644 --- a/src/pages/AboutPage.tsx +++ b/src/pages/AboutPage.tsx @@ -1,10 +1,12 @@ -import React from 'react'; -import FoxCard from '../components/FoxCard'; import { Heart, Gamepad2, Code, Music } from 'lucide-react'; -import { SpotifyVisualizer } from '../components/SpotifyVisualizer'; +import FoxCard from '@/components/FoxCard'; +import SpotifyVisualizer from '@/components/SpotifyVisualizer'; +import useSpotifyData from '@/hooks/useSpotifyData'; const AboutPage = () => { - const calculateAge = () => { + const { data: spotifyData, loading } = useSpotifyData(); + + const calculateAge = (): number => { const birthDate = new Date("2009-05-15"); const today = new Date(); let age = today.getFullYear() - birthDate.getFullYear(); @@ -19,7 +21,9 @@ const AboutPage = () => {

About Me

-

Transfem Foxgirl • {calculateAge()} years old • Programmer & Streamer

+

+ Transfem Foxgirl • {calculateAge()} years old • Programmer & Streamer +

@@ -40,7 +44,18 @@ const AboutPage = () => {

Streaming

-

Find me on Twitch playing FiveM and other games!

+

+ Find me on{' '} + + Twitch + + {' '}playing FiveM and other games! +

@@ -48,7 +63,7 @@ const AboutPage = () => {

Current Tunes

- +
@@ -56,4 +71,3 @@ const AboutPage = () => { }; export default AboutPage; - diff --git a/src/styles/animations.css b/src/styles/animations.css new file mode 100644 index 0000000..7a816a9 --- /dev/null +++ b/src/styles/animations.css @@ -0,0 +1,31 @@ +@keyframes particleFloat { + 0% { + transform: translateY(100vh) scale(0); + opacity: 0; + } + 50% { + opacity: 0.5; + } + 100% { + transform: translateY(-20vh) scale(1); + opacity: 0; + } +} + +@keyframes glow { + 0%, 100% { + filter: drop-shadow(0 0 2px var(--accent-neon)); + } + 50% { + filter: drop-shadow(0 0 8px var(--accent-neon)); + } +} + +@keyframes bounce { + 0%, 100% { + transform: translateY(0); + } + 50% { + transform: translateY(-10px); + } +} diff --git a/src/styles/base.css b/src/styles/base.css new file mode 100644 index 0000000..75c65c8 --- /dev/null +++ b/src/styles/base.css @@ -0,0 +1,49 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +@layer base { + :root { + --background-primary: #1a0b2e; + --background-secondary: #2f1c54; + --accent-primary: #9d4edd; + --accent-neon: #b249f8; + --text-glow: #e0aaff; + --text-primary: #ffffff; + --dark-accent: #240046; + --fox-pink: #ffc6e5; + --fox-pink-glow: #ffadd6; + --fox-orange: #ff9466; + --fox-white: #fff5f9; + } + + body { + @apply bg-background-primary text-text-primary; + font-family: 'Inter', sans-serif; + } +} + +@layer components { + .page-container { + @apply max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8; + } + + .nav-link { + @apply flex items-center gap-2 px-4 py-2 rounded-lg transition-all + hover:bg-accent-primary/10 hover:text-accent-neon; + } + + .nav-link.active { + @apply bg-accent-primary/20 text-accent-neon; + } + + .fox-card { + @apply relative bg-gradient-card rounded-xl p-6 border border-accent-primary/20 + transition-all hover:border-accent-neon/40 hover:shadow-lg + hover:shadow-accent-primary/10; + } + + .text-glow { + @apply text-text-primary drop-shadow-[0_0_10px_rgba(224,170,255,0.5)]; + } +} diff --git a/src/styles/cursor.css b/src/styles/cursor.css index 7c649b0..dbb6651 100644 --- a/src/styles/cursor.css +++ b/src/styles/cursor.css @@ -1,15 +1,13 @@ -/* Base cursor */ * { - cursor: url("cursor/default.svg") 16 16, auto; + cursor: url('/cursors/default.svg') 16 16, auto; } -/* Clickable elements cursor */ -a, button, [role="button"], input[type="submit"], -input[type="button"], select { - cursor: url("cursor/paw.svg") 16 16, pointer; +a, button, [role="button"], +input[type="submit"], input[type="button"], +select { + cursor: url('/cursors/paw.svg') 16 16, pointer; } -/* Loading cursor */ .loading { - cursor: url("cursor/tail-loading.svg") 16 16, progress; + cursor: url('/cursors/tail-loading.svg') 16 16, progress; } diff --git a/src/styles/index.css b/src/styles/index.css index ec2585e..7c4ca1b 100644 --- a/src/styles/index.css +++ b/src/styles/index.css @@ -1,13 +1,4 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} +@import 'base.css'; +@import 'animations.css'; +@import 'utilities.css'; +@import 'cursor.css'; diff --git a/src/styles/utilities.css b/src/styles/utilities.css new file mode 100644 index 0000000..d3111df --- /dev/null +++ b/src/styles/utilities.css @@ -0,0 +1,27 @@ +@layer utilities { + .animated-bg { + @apply relative bg-gradient-primary overflow-hidden; + } + + .animated-bg::before { + content: ''; + @apply absolute inset-0 bg-gradient-primary opacity-50; + animation: gradientShift 15s ease infinite; + } + + .fox-ear { + @apply absolute w-8 h-8 bg-fox-pink opacity-10 transition-opacity; + } + + .fox-ear-left { + @apply -top-4 -left-4 transform rotate-45 rounded-bl-xl; + } + + .fox-ear-right { + @apply -top-4 -right-4 transform -rotate-45 rounded-br-xl; + } + + .fox-card:hover .fox-ear { + @apply opacity-20; + } +} diff --git a/src/types/index.ts b/src/types/index.ts index 42ae1dd..93d7e45 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,20 +1,23 @@ // src/types/index.ts -export interface GithubRepo { - id: number; - name: string; - description: string | null; - html_url: string; - language: string | null; - languages: string[]; -} +import { ReactNode } from 'react'; export interface SpotifyTrack { id: string; name: string; - artists: Array<{ + artists: { name: string; - }>; + }[]; +} + +export interface GithubRepo { + id: number; + name: string; + html_url: string; + description: string | null; + language: string | null; + languages_url: string; + languages: string[]; } export interface ErrorBoundaryState { @@ -23,10 +26,20 @@ export interface ErrorBoundaryState { } export interface ErrorBoundaryProps { - children: React.ReactNode; + children: ReactNode; } export interface FoxCardProps { - children: React.ReactNode; + children: ReactNode; className?: string; } + +export interface SpotifyVisualizerProps { + isPlaying?: boolean; +} + +export interface ThemeColors { + primary: string; + secondary: string; + accent: string; +} diff --git a/src/worker/index.ts b/src/worker/index.ts new file mode 100644 index 0000000..d692e3b --- /dev/null +++ b/src/worker/index.ts @@ -0,0 +1,24 @@ +// src/worker/index.ts +import { Router } from 'itty-router'; +import type { Env } from './types'; +import { errorResponse, corsResponse } from './utils/response'; +import { spotifyRouter } from './routes/spotify'; +import { healthRouter } from './routes/health'; + +const router = Router(); + +// CORS preflight +router.options('*', () => corsResponse()); + +// Mount route handlers +router.get('/health/*', healthRouter.handle); +router.get('/spotify/*', spotifyRouter.handle); + +// 404 handler +router.all('*', () => errorResponse('Not Found', 404)); + +export default { + fetch: (request: Request, env: Env, ctx: ExecutionContext) => + router.handle(request, env, ctx) + .catch(error => errorResponse(error instanceof Error ? error.message : 'Internal Server Error', 500)) +}; diff --git a/src/worker/types/index.ts b/src/worker/types/index.ts new file mode 100644 index 0000000..1a20c91 --- /dev/null +++ b/src/worker/types/index.ts @@ -0,0 +1,18 @@ +// src/worker/types/index.ts +export interface Env { + SPOTIFY_CLIENT_ID: string; + SPOTIFY_CLIENT_SECRET: string; + SPOTIFY_REDIRECT_URI: string; + ENVIRONMENT: 'development' | 'production'; +} + +export interface ApiResponse { + data?: T; + error?: string; + status: number; +} + +export interface HealthCheckResponse { + status: 'healthy' | 'unhealthy'; + timestamp: string; +} diff --git a/src/worker/types/spotify.ts b/src/worker/types/spotify.ts new file mode 100644 index 0000000..eae14a5 --- /dev/null +++ b/src/worker/types/spotify.ts @@ -0,0 +1,42 @@ +// src/worker/types/spotify.ts +export interface SpotifyTokenResponse { + access_token: string; + token_type: string; + expires_in: number; +} + +export interface SpotifyError { + error: { + status: number; + message: string; + }; +} + +export interface SpotifyArtist { + name: string; + id: string; + uri: string; +} + +export interface SpotifyTrack { + id: string; + name: string; + artists: SpotifyArtist[]; + album: { + name: string; + images: Array<{ + url: string; + height: number; + width: number; + }>; + }; + duration_ms: number; + uri: string; +} + +export interface SpotifyTopTracksResponse { + items: SpotifyTrack[]; + total: number; + limit: number; + offset: number; +} diff --git a/src/worker/utils/response.ts b/src/worker/utils/response.ts new file mode 100644 index 0000000..df1e316 --- /dev/null +++ b/src/worker/utils/response.ts @@ -0,0 +1,24 @@ +// src/worker/utils/response.ts +const corsHeaders = { + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET, POST, OPTIONS', + 'Access-Control-Allow-Headers': 'Content-Type, Authorization', +}; + +export const jsonResponse = (data: T, status = 200): Response => { + return new Response(JSON.stringify(data), { + status, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + }, + }); +}; + +export const errorResponse = (message: string, status = 500): Response => { + return jsonResponse({ error: message, status }, status); +}; + +export const corsResponse = (): Response => { + return new Response(null, { headers: corsHeaders }); +}; diff --git a/src/worker/utils/spotify.ts b/src/worker/utils/spotify.ts new file mode 100644 index 0000000..9279136 --- /dev/null +++ b/src/worker/utils/spotify.ts @@ -0,0 +1,31 @@ +// src/worker/utils/spotify.ts +import type { Env } from '../types'; +import type { SpotifyTokenResponse, SpotifyError } from '../types/spotify'; +import { errorResponse } from './response'; + +export async function getSpotifyToken(env: Env): Promise { + try { + const response = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': `Basic ${btoa(`${env.SPOTIFY_CLIENT_ID}:${env.SPOTIFY_CLIENT_SECRET}`)}` + }, + body: 'grant_type=client_credentials' + }); + + if (!response.ok) { + throw new Error(`Failed to get token: ${response.status}`); + } + + const data = await response.json(); + + if ('error' in data) { + throw new Error(data.error.message); + } + + return data.access_token; + } catch (error) { + throw new Error(`Failed to get Spotify token: ${error instanceof Error ? error.message : 'Unknown error'}`); + } +} diff --git a/tailwind.config.js b/tailwind.config.js index 1eea89b..f89f448 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -1,4 +1,4 @@ -/** @type {import('tailwindcss').Config} */ +/** @type {import('tailwindcss').Config} */ export default { content: [ "./index.html", @@ -14,6 +14,30 @@ export default { 'text-glow': '#e0aaff', 'text-primary': '#ffffff', 'dark-accent': '#240046', + 'fox-pink': '#ffc6e5', + 'fox-pink-glow': '#ffadd6', + 'fox-orange': '#ff9466', + 'fox-white': '#fff5f9', + }, + animation: { + 'bounce-slow': 'bounce 3s linear infinite', + 'glow': 'glow 2s ease-in-out infinite', + 'float': 'float 3s ease-in-out infinite', + }, + keyframes: { + glow: { + '0%, 100%': { filter: 'drop-shadow(0 0 2px var(--accent-neon))' }, + '50%': { filter: 'drop-shadow(0 0 8px var(--accent-neon))' }, + }, + float: { + '0%, 100%': { transform: 'translateY(0)' }, + '50%': { transform: 'translateY(-10px)' }, + }, + }, + backgroundImage: { + 'gradient-primary': 'linear-gradient(135deg, var(--background-primary) 0%, var(--background-secondary) 100%)', + 'gradient-card': 'linear-gradient(135deg, rgba(47, 28, 84, 0.3) 0%, rgba(157, 78, 221, 0.1) 100%)', + 'gradient-hover': 'linear-gradient(135deg, rgba(157, 78, 221, 0.2) 0%, rgba(178, 73, 248, 0.1) 100%)', }, }, }, diff --git a/tsconfig.worker.json b/tsconfig.worker.json new file mode 100644 index 0000000..1c70b7a --- /dev/null +++ b/tsconfig.worker.json @@ -0,0 +1,26 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ESNext"], + "types": ["@cloudflare/workers-types"], + "noEmit": true, + "isolatedModules": true, + "allowJs": true, + "checkJs": false, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "strictBindCallApply": true, + "strictPropertyInitialization": true, + "noImplicitThis": true, + "alwaysStrict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true + }, + "include": ["src/worker/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/wrangler.toml b/wrangler.toml index 3542c16..d88518e 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -1,22 +1,17 @@ name = "personal-site" -main = "spotify-worker.js" +main = "src/worker/index.ts" compatibility_date = "2024-02-01" [build] -command = "npm run build" -watch_dir = "src" - -[site] -bucket = "./dist" +command = "npm run build:worker" [env.production] name = "personal-site" -routes = ["personal-site.pages.dev/*"] vars = { ENVIRONMENT = "production" } [env.development] name = "personal-site-dev" vars = { ENVIRONMENT = "development" } -[vars] -ENVIRONMENT = "development" +[dev] +port = 8787