mirror of
https://github.com/System-End/My-website.git
synced 2026-04-19 16:28:16 +00:00
.
This commit is contained in:
parent
1d2f7679c8
commit
41b31ad4c8
25 changed files with 536 additions and 211 deletions
187
package.json
187
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"
|
||||
]
|
||||
}
|
||||
}
|
||||
"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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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
31
src/styles/animations.css
Normal 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
49
src/styles/base.css
Normal 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)];
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
27
src/styles/utilities.css
Normal 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;
|
||||
}
|
||||
}
|
||||
|
|
@ -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
24
src/worker/index.ts
Normal 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
18
src/worker/types/index.ts
Normal 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;
|
||||
}
|
||||
42
src/worker/types/spotify.ts
Normal file
42
src/worker/types/spotify.ts
Normal 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;
|
||||
}
|
||||
24
src/worker/utils/response.ts
Normal file
24
src/worker/utils/response.ts
Normal 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 });
|
||||
};
|
||||
31
src/worker/utils/spotify.ts
Normal file
31
src/worker/utils/spotify.ts
Normal 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'}`);
|
||||
}
|
||||
}
|
||||
|
|
@ -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
26
tsconfig.worker.json
Normal 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"]
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue