diff --git a/cloudflare-worker.js b/cloudflare-worker.js new file mode 100644 index 0000000..35500d4 --- /dev/null +++ b/cloudflare-worker.js @@ -0,0 +1,50 @@ + +import { Router } from 'itty-router'; + +const router = Router(); + +const CLIENT_ID = 'YOUR_SPOTIFY_CLIENT_ID'; +const CLIENT_SECRET = 'YOUR_SPOTIFY_CLIENT_SECRET'; +const REDIRECT_URI = 'YOUR_REDIRECT_URI'; +let accessToken = null; + +// Function to refresh Spotify Access Token +async function refreshAccessToken() { + const authResponse = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic ' + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`) + }, + body: 'grant_type=client_credentials' + }); + + const data = await authResponse.json(); + accessToken = data.access_token; +} + +// Spotify Data Fetch Endpoint +router.get('/spotify-data', async (request) => { + if (!accessToken) { + await refreshAccessToken(); + } + + const spotifyResponse = await fetch('https://api.spotify.com/v1/me/top/tracks?limit=10', { + headers: { + 'Authorization': `Bearer ${accessToken}` + } + }); + + const spotifyData = await spotifyResponse.json(); + return new Response(JSON.stringify(spotifyData), { + headers: { 'Content-Type': 'application/json' } + }); +}); + +// Default Route +router.all('*', () => new Response('Not Found', { status: 404 })); + +// Event Listener for Worker Requests +addEventListener('fetch', (event) => { + event.respondWith(router.handle(event.request)); +}); diff --git a/deploy.ps1 b/deploy.ps1 index 7fac0c4..21c2611 100644 --- a/deploy.ps1 +++ b/deploy.ps1 @@ -8,7 +8,7 @@ npm run build # Step 3: Deploy Frontend to Cloudflare Pages Write-Output "Deploying frontend to Cloudflare Pages..." -wrangler pages deploy ./build --project-name personal-site +wrangler pages deploy ./build --project-name personal-site-test # Step 4: Deploy Backend to Cloudflare Workers Write-Output "Deploying backend worker..." diff --git a/package.json b/package.json index 6240241..29eca1e 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,22 @@ "version": "0.1.0", "private": true, "dependencies": { + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@eslint/eslintrc": "^1.4.1", + "@eslint/js": "^8.57.1", + "@jridgewell/sourcemap-codec": "^1.5.0", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@eslint/eslintrc": "^1.4.1", + "@eslint/js": "^8.57.1", + "@jridgewell/sourcemap-codec": "^1.5.0", "cra-template": "1.2.0", "react": "^18.2.0", "react-dom": "^18.2.0", diff --git a/public/manifest.json b/public/manifest.json index 080d6c7..e6c7b95 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,6 +1,6 @@ { - "short_name": "React App", - "name": "Create React App Sample", + "short_name": "Personal Website", + "name": "My Personal Website", "icons": [ { "src": "favicon.ico", @@ -8,12 +8,12 @@ "type": "image/x-icon" }, { - "src": "logo192.png", + "src": "logo.png", "type": "image/png", "sizes": "192x192" }, { - "src": "logo512.png", + "src": "logo.png", "type": "image/png", "sizes": "512x512" } diff --git a/spotify-worker.js b/spotify-worker.js new file mode 100644 index 0000000..24b7020 --- /dev/null +++ b/spotify-worker.js @@ -0,0 +1,73 @@ + +import { Router } from 'itty-router'; + +const router = Router(); + +const CLIENT_ID = 'YOUR_SPOTIFY_CLIENT_ID'; +const CLIENT_SECRET = 'YOUR_SPOTIFY_CLIENT_SECRET'; + +let accessToken = null; +let tokenExpiry = null; + +// Helper function to fetch a new access token +async function fetchAccessToken() { + const response = await fetch('https://accounts.spotify.com/api/token', { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Authorization': 'Basic ' + btoa(`${CLIENT_ID}:${CLIENT_SECRET}`) + }, + body: 'grant_type=client_credentials' + }); + + if (!response.ok) { + throw new Error(`Failed to fetch Spotify token: ${response.statusText}`); + } + + const data = await response.json(); + accessToken = data.access_token; + tokenExpiry = Date.now() + (data.expires_in * 1000); + console.log('New access token fetched'); +} + +// Middleware to ensure a valid access token +async function ensureAccessToken() { + if (!accessToken || Date.now() >= tokenExpiry) { + await fetchAccessToken(); + } +} + +// Route to fetch Spotify data (Top Tracks) +router.get('/spotify-data', async () => { + try { + await ensureAccessToken(); + + const response = await fetch('https://api.spotify.com/v1/me/top/tracks?limit=10', { + headers: { 'Authorization': `Bearer ${accessToken}` } + }); + + if (!response.ok) { + return new Response(JSON.stringify({ error: 'Failed to fetch Spotify data' }), { status: response.status }); + } + + const data = await response.json(); + return new Response(JSON.stringify({ + topTracks: data.items.map(track => ({ + name: track.name, + artist: track.artists.map(artist => artist.name).join(', ') + })) + }), { headers: { 'Content-Type': 'application/json' } }); + + } catch (error) { + console.error('Error fetching Spotify data:', error); + return new Response(JSON.stringify({ error: 'Internal server error' }), { status: 500 }); + } +}); + +// Default route for unmatched paths +router.all('*', () => new Response('Not Found', { status: 404 })); + +// Event listener for handling requests +addEventListener('fetch', (event) => { + event.respondWith(router.handle(event.request)); +}); diff --git a/src/App.css b/src/App.css index 74b5e05..8871b1b 100644 --- a/src/App.css +++ b/src/App.css @@ -5,6 +5,9 @@ .App-logo { height: 40vmin; pointer-events: none; + background-image: url('/logo.png'); + background-size: contain; + background-repeat: no-repeat; } @media (prefers-reduced-motion: no-preference) { diff --git a/src/App.js b/src/App.js index 164fba9..c90e6ed 100644 --- a/src/App.js +++ b/src/App.js @@ -1,21 +1,42 @@ -import React from 'react'; -import Navbar from './components/Navbar'; -import SpotifyList from './components/SpotifyList'; -import GitHubRepos from './components/GitHubRepos'; -import './index.css'; +import React, { useEffect } from 'react'; +import './rolling-code.css'; +import logo from './logo.png'; -function App() { - return ( -
- -
-

Welcome to my resume website

-

This is the homepage. It is currently a WIP.

-
- - -
- ); -} +const App = () => { + useEffect(() => { + // Generate rolling code lines + const container = document.querySelector('.rolling-code-container'); + for (let i = 0; i < 30; i++) { + const line = document.createElement('div'); + line.className = 'code-line'; + line.style.animationDelay = `${Math.random() * 5}s`; + line.textContent = Math.random().toString(36).substr(2, 80); + container.appendChild(line); + } + + // Generate particles + const particleContainer = document.querySelector('.particle-container'); + for (let i = 0; i < 50; i++) { + const particle = document.createElement('div'); + particle.className = 'particle'; + particle.style.left = `${Math.random() * 100}vw`; + particle.style.animationDelay = `${Math.random() * 10}s`; + particleContainer.appendChild(particle); + } + }, []); + + return ( +
+ Logo +
+
+
+
+

Welcome to My Website

+

Enhanced Background with Lighting Effects

+
+
+ ); +}; export default App; diff --git a/src/components/GithubRepos.css b/src/components/GithubRepos.css new file mode 100644 index 0000000..985e6cf --- /dev/null +++ b/src/components/GithubRepos.css @@ -0,0 +1,54 @@ + +.github-repos-container { + padding: 2rem; + background-color: #f9f9f9; + font-family: Arial, sans-serif; + text-align: center; +} + +.repos-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 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; +} + +.repo-card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); +} + +.repo-name { + font-size: 1.2rem; + font-weight: bold; + color: #0077cc; + text-decoration: none; +} + +.repo-name:hover { + text-decoration: underline; +} + +.repo-description { + color: #555; + margin-top: 0.5rem; +} + +.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; +} diff --git a/src/components/GithubRepos.js b/src/components/GithubRepos.js index 89a8049..8371f12 100644 --- a/src/components/GithubRepos.js +++ b/src/components/GithubRepos.js @@ -1,29 +1,44 @@ + import React, { useEffect, useState } from 'react'; +import './GithubRepos.css'; -function GitHubRepos() { - const [repos, setRepos] = useState([]); +const GithubRepos = () => { + const [repos, setRepos] = useState([]); - useEffect(() => { - fetch('/github-repos') - .then(response => response.json()) - .then(data => { - setRepos(data); - }); - }, []); + useEffect(() => { + const fetchRepos = async () => { + try { + const response = await fetch('https://api.github.com/users/EndofTimee/repos'); + const data = await response.json(); + setRepos(data); + } catch (error) { + console.error('Error fetching GitHub repos:', error); + } + }; - return ( -
-

My GitHub Repositories

- -
- ); -} + fetchRepos(); + }, []); -export default GitHubRepos; \ No newline at end of file + return ( +
+

My GitHub Repositories

+
+ {repos.map((repo) => ( +
+ + {repo.name} + +

+ {repo.description || 'No description provided.'} +

+ {repo.language && ( + {repo.language} + )} +
+ ))} +
+
+ ); +}; + +export default GithubRepos; diff --git a/src/components/LoadingScreen.js b/src/components/LoadingScreen.js new file mode 100644 index 0000000..d4a94c7 --- /dev/null +++ b/src/components/LoadingScreen.js @@ -0,0 +1,14 @@ + +import React from 'react'; +import './LoadingAnimation.css'; + +const LoadingAnimation = () => { + return ( +
+
+

Loading...

+
+ ); +}; + +export default LoadingAnimation; diff --git a/src/components/ParallaxEffect.js b/src/components/ParallaxEffect.js new file mode 100644 index 0000000..6f5e386 --- /dev/null +++ b/src/components/ParallaxEffect.js @@ -0,0 +1,20 @@ + +import React from 'react'; +import './parallax.css'; + +const ParallaxEffect = () => { + return ( +
+
+
+
+
+

Welcome to the Parallax Effect

+

Scroll down to see the magic happen!

+
+
+
+ ); +}; + +export default ParallaxEffect; diff --git a/src/components/loading-screen.css b/src/components/loading-screen.css new file mode 100644 index 0000000..d363b25 --- /dev/null +++ b/src/components/loading-screen.css @@ -0,0 +1,24 @@ + +.loading-container { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + height: 100vh; + background: black; + color: white; +} + +.loading-spinner { + width: 50px; + height: 50px; + border: 5px solid rgba(255, 255, 255, 0.3); + border-top: 5px solid white; + border-radius: 50%; + animation: spin 1s linear infinite; +} + +@keyframes spin { + 0% { transform: rotate(0deg); } + 100% { transform: rotate(360deg); } +} diff --git a/src/parallax-effect.css b/src/parallax-effect.css new file mode 100644 index 0000000..a9077b5 --- /dev/null +++ b/src/parallax-effect.css @@ -0,0 +1,52 @@ + +body, html { + margin: 0; + padding: 0; + height: 100%; + overflow-x: hidden; + font-family: Arial, sans-serif; +} + +.parallax-container { + position: relative; + width: 100%; + height: 100vh; + overflow: hidden; + perspective: 2px; /* Creates the parallax effect */ +} + +.parallax-layer { + position: absolute; + width: 100%; + height: 100%; + background-size: cover; + background-position: center; + transform: translateZ(0); + z-index: -1; +} + +.layer-1 { + background-image: url('https://source.unsplash.com/1600x900/?nature'); + transform: translateZ(-1px) scale(1.5); +} + +.layer-2 { + background-image: url('https://source.unsplash.com/1600x900/?forest'); + transform: translateZ(-0.5px) scale(1.2); +} + +.content-layer { + position: relative; + z-index: 1; + text-align: center; + color: white; + font-size: 2rem; + padding-top: 40vh; +} + +.parallax-text { + background: rgba(0, 0, 0, 0.5); + padding: 1rem 2rem; + display: inline-block; + border-radius: 8px; +} diff --git a/src/rolling-effects.css b/src/rolling-effects.css new file mode 100644 index 0000000..b04e553 --- /dev/null +++ b/src/rolling-effects.css @@ -0,0 +1,88 @@ + +@keyframes rollingCode { + 0% { transform: translateY(0); opacity: 1; } + 100% { transform: translateY(100vh); opacity: 0; } +} + +@keyframes particleMove { + 0% { transform: translateY(0) translateX(0); opacity: 0.5; } + 50% { opacity: 1; } + 100% { transform: translateY(100vh) translateX(10px); opacity: 0; } +} + +@keyframes lightAnimation { + 0% { + background-position: 0% 0%; + } + 50% { + background-position: 100% 100%; + } + 100% { + background-position: 0% 0%; + } +} + +body { + margin: 0; + overflow: hidden; + background: black; + font-family: monospace; + font-size: 1rem; + color: limegreen; + position: relative; + height: 100vh; +} + +.animated-lighting { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background: radial-gradient(circle, rgba(0, 255, 0, 0.3), transparent 60%); + background-size: 300% 300%; + animation: lightAnimation 12s infinite linear; + z-index: 0; +} + +.code-line { + position: absolute; + top: -10%; + left: 0; + right: 0; + width: 100%; + white-space: nowrap; + overflow: hidden; + animation: rollingCode 10s linear infinite; + opacity: 0.7; + z-index: 1; + color: #00ff00; +} + +.particle { + position: absolute; + width: 5px; + height: 5px; + background: rgba(255, 255, 255, 0.5); + border-radius: 50%; + animation: particleMove 8s linear infinite; + z-index: 2; +} + +.particle:nth-child(odd) { + animation-duration: 12s; +} + +.particle:nth-child(even) { + animation-duration: 10s; + width: 4px; + height: 4px; +} + +.content { + position: relative; + z-index: 3; + color: white; + text-align: center; + margin-top: 20vh; +}