This commit is contained in:
Unknown 2025-02-01 08:39:04 -07:00 committed by End
parent 6355b1a226
commit fd6e3b911d
No known key found for this signature in database
25 changed files with 536 additions and 211 deletions

View file

@ -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"
]
}
}
"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"
]
}
}

View file

@ -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<ErrorBoundaryProps, ErrorBoundaryState> {
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;

View file

@ -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 (
<div className={`fox-card ${className}`}>
<div className="fox-ear fox-ear-left" />
<div className="fox-ear fox-ear-right" />
{children}
</div>
);
};
const FoxCard = ({ children, className = '' }: FoxCardProps) => (
<div className={`fox-card ${className}`.trim()}>
<div className="fox-ear fox-ear-left" />
<div className="fox-ear fox-ear-right" />
{children}
</div>
);
export default FoxCard;

View file

@ -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<GithubRepo[]>([]);
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;

View file

@ -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;

View file

@ -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 = () => {
<span>Stream</span>
</a>
</div>
<ThemeToggle />
</div>
</nav>
);

View file

@ -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<HTMLCanvasElement>(null);
const animationRef = useRef<number | null>(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 (
<div className="visualizer-container">
<canvas
ref={canvasRef}
width={300}
height={60}
ref={canvasRef}
width={300}
height={60}
className="music-visualizer"
/>
</div>
@ -57,4 +64,3 @@ const SpotifyVisualizer = ({ isPlaying }) => {
};
export default SpotifyVisualizer;

View file

@ -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;

View file

@ -1,9 +1,10 @@
import { useState, useEffect } from 'react';
import { GithubRepo } from '@/types';
const useGithubRepos = () => {
const [repos, setRepos] = useState([]);
const [repos, setRepos] = useState<GithubRepo[]>([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [error, setError] = useState<string | null>(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;

View file

@ -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<SpotifyTrack[] | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [error, setError] = useState<string | null>(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;

View file

@ -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 = () => {
<div className="page-container">
<FoxCard className="header-card">
<h1 className="text-glow">About Me</h1>
<p className="text-gradient">Transfem Foxgirl {calculateAge()} years old Programmer & Streamer</p>
<p className="text-gradient">
Transfem Foxgirl {calculateAge()} years old Programmer & Streamer
</p>
</FoxCard>
<div className="content-grid">
@ -40,7 +44,18 @@ const AboutPage = () => {
<Gamepad2 size={24} className="text-accent-primary" />
<h2>Streaming</h2>
</div>
<p>Find me on <a href="https://twitch.tv/EndofTimee" className="text-accent-neon hover:text-glow" target="_blank" rel="noopener noreferrer">Twitch</a> playing FiveM and other games!</p>
<p>
Find me on{' '}
<a
href="https://twitch.tv/EndofTimee"
className="text-accent-neon hover:text-glow"
target="_blank"
rel="noopener noreferrer"
>
Twitch
</a>
{' '}playing FiveM and other games!
</p>
</FoxCard>
<FoxCard>
@ -48,7 +63,7 @@ const AboutPage = () => {
<Music size={24} className="text-accent-primary" />
<h2>Current Tunes</h2>
</div>
<SpotifyVisualizer />
<SpotifyVisualizer isPlaying={!loading && !!spotifyData} />
</FoxCard>
</div>
</div>
@ -56,4 +71,3 @@ const AboutPage = () => {
};
export default AboutPage;

31
src/styles/animations.css Normal file
View file

@ -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);
}
}

49
src/styles/base.css Normal file
View file

@ -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)];
}
}

View file

@ -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;
}

View file

@ -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';

27
src/styles/utilities.css Normal file
View file

@ -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;
}
}

View file

@ -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;
}

24
src/worker/index.ts Normal file
View file

@ -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))
};

18
src/worker/types/index.ts Normal file
View file

@ -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<T> {
data?: T;
error?: string;
status: number;
}
export interface HealthCheckResponse {
status: 'healthy' | 'unhealthy';
timestamp: string;
}

View file

@ -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;
}

View file

@ -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 = <T>(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 });
};

View file

@ -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<string> {
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<SpotifyTokenResponse | SpotifyError>();
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'}`);
}
}

View file

@ -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%)',
},
},
},

26
tsconfig.worker.json Normal file
View file

@ -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"]
}

View file

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