This commit is contained in:
Unknown 2025-01-31 18:36:47 -07:00 committed by End
parent 5fc3529d7e
commit 9d73d0c7bf
No known key found for this signature in database
20 changed files with 456 additions and 251 deletions

View file

@ -1,114 +1,34 @@
import React, { useEffect, useState } from 'react';
import './styles/App.css';
import SpotifyList from './components/SpotifyList';
import LoadingAnimation from './components/LoadingAnimation';
import GithubRepos from './components/GithubRepos';
import { Music, Code, Twitch, Github, Cpu, Shield } from 'lucide-react';
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Navbar from './components/Navbar';
import AboutPage from './pages/AboutPage';
import APCSPPage from './pages/APCSPPage';
import ProjectsPage from './pages/ProjectsPage';
import ErrorBoundary from './components/ErrorBoundary';
import './App.css';
const App = () => {
const [age, setAge] = useState(0);
useEffect(() => {
// Age calculation
const calculateAge = () => {
const birthDate = new Date('2009-05-15');
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
};
setAge(calculateAge());
// Particle effect setup
const createParticles = () => {
const particleContainer = document.querySelector('.particle-container');
if (particleContainer) {
for (let i = 0; i < 50; i++) {
const particle = document.createElement('div');
particle.className = 'particle';
particle.style.left = `${Math.random() * 100}vw`;
particle.style.animationDuration = `${Math.random() * 3 + 2}s`;
particle.style.animationDelay = `${Math.random() * 2}s`;
particleContainer.appendChild(particle);
}
}
};
createParticles();
}, []);
const interests = [
{ icon: <Code size={24} />, title: 'Programming', description: 'Full-stack development & coding projects' },
{ icon: <Cpu size={24} />, title: 'Robotics', description: 'Building & programming robots' },
{ icon: <Shield size={24} />, title: 'Cybersecurity', description: 'Network security & ethical hacking' },
{ icon: <Music size={24} />, title: 'Music', description: 'Music production & listening' },
{ icon: <Twitch size={24} />, title: 'Streaming', description: 'FiveM & variety gaming on Twitch' },
{ icon: <Github size={24} />, title: 'Open Source', description: 'Contributing to GitHub projects' }
];
return (
<div className="app-container animated-bg">
<div className="particle-container" />
{/* Header Section */}
<header className="header neon-text">
<h1>EndofTimee</h1>
<p className="subtitle">Programmer Streamer Foxgirl 🦊</p>
</header>
{/* About Section */}
<section className="content-section about-section">
<h2 className="neon-text">About Me</h2>
<div className="about-content">
<p>Hey there! I'm a {age}-year-old transfem programmer and content creator.
When I'm not coding or building robots, you can find me streaming on
<a href="https://twitch.tv/EndofTimee" target="_blank" rel="noopener noreferrer"
className="twitch-link">Twitch</a>!</p>
<ErrorBoundary>
<Router>
<div className="app-container animated-bg">
<Navbar />
<main className="main-content">
<Routes>
<Route path="/" element={<AboutPage />} />
<Route path="/apcsp" element={<APCSPPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="*" element={
<div className="error-page">
<h1 className="text-glow">404: Page Not Found</h1>
<p>Oops! This fox couldn't find what you're looking for.</p>
</div>
} />
</Routes>
</main>
</div>
</section>
{/* Interests Grid */}
<section className="content-section interests-section">
<h2 className="neon-text">What I Do</h2>
<div className="interests-grid">
{interests.map((interest, index) => (
<div key={index} className="interest-card">
<div className="interest-icon">{interest.icon}</div>
<h3>{interest.title}</h3>
<p>{interest.description}</p>
</div>
))}
</div>
</section>
{/* Streaming Section */}
<section className="content-section stream-section">
<h2 className="neon-text">Streaming</h2>
<div className="stream-content">
<p>Join me on Twitch for FiveM roleplay and various other games!
I love interacting with chat and building a positive community.</p>
<a href="https://twitch.tv/EndofTimee" target="_blank"
rel="noopener noreferrer" className="twitch-button">
<Twitch className="icon" />
Watch Live
</a>
</div>
</section>
{/* GitHub Section */}
<div className="github-section">
<GithubRepos />
</div>
{/* Music Section */}
<div className="music-section">
<SpotifyList />
</div>
</div>
</Router>
</ErrorBoundary>
);
};

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M50 10 C20 10 10 40 10 50 C10 80 40 90 50 90 C60 90 90 80 90 50 C90 40 80 10 50 10" fill="#ff9466"/>
<path d="M30 50 L40 40 L30 30" stroke="#240046" stroke-width="3" fill="none"/>
<path d="M70 50 L60 40 L70 30" stroke="#240046" stroke-width="3" fill="none"/>
<rect x="45" y="60" width="10" height="2" fill="#240046"/>
</svg>

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M50 10 C20 10 10 40 10 50 C10 80 40 90 50 90 C60 90 90 80 90 50 C90 40 80 10 50 10" fill="#ff9466"/>
<circle cx="35" cy="40" r="5" fill="#240046"/>
<circle cx="65" cy="40" r="5" fill="#240046"/>
<path d="M35 60 Q50 65 65 60" stroke="#240046" stroke-width="3" fill="none"/>
<rect x="30" y="30" width="40" height="20" rx="5" fill="#240046"/>
</svg>

View file

@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M50 10 C20 10 10 40 10 50 C10 80 40 90 50 90 C60 90 90 80 90 50 C90 40 80 10 50 10" fill="#ff9466"/>
<circle cx="35" cy="40" r="5" fill="#240046"/>
<circle cx="65" cy="40" r="5" fill="#240046"/>
<path d="M35 60 Q50 70 65 60" stroke="#240046" stroke-width="3" fill="none"/>
</svg>

View file

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
<path d="M50 10 C20 10 10 40 10 50 C10 80 40 90 50 90 C60 90 90 80 90 50 C90 40 80 10 50 10" fill="#ff9466"/>
<circle cx="35" cy="40" r="5" fill="#240046"/>
<circle cx="65" cy="40" r="5" fill="#240046"/>
<path d="M35 60 Q50 70 65 60" stroke="#240046" stroke-width="3" fill="none"/>
<path d="M75 30 L65 35 L65 25 L75 20 Z" fill="#240046"/>
</svg>

View file

@ -1,14 +0,0 @@
import React from 'react';
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>
);
};
export default FoxCard;

View file

@ -0,0 +1,25 @@
import React from 'react';
import '../styles/LoadingFox.css';
const LoadingFox = () => {
return (
<div className="loading-fox-container">
<div className="fox-loader">
<div className="fox-face">
<div className="fox-ears">
<div className="ear left"></div>
<div className="ear right"></div>
</div>
<div className="fox-eyes">
<div className="eye left"></div>
<div className="eye right"></div>
</div>
<div className="fox-nose"></div>
</div>
<div className="loading-text">Loading...</div>
</div>
</div>
);
};
export default LoadingFox;

View file

@ -1,14 +0,0 @@
import React from 'react';
import './LoadingAnimation.css';
const LoadingAnimation = () => {
return (
<div className="loading-container">
<div className="loading-spinner"></div>
<p>Loading...</p>
</div>
);
};
export default LoadingAnimation;

View file

@ -1,16 +1,59 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import ThemeToggle from './ThemeToggle';
import { Home, Code, BookOpen, Twitch } from 'lucide-react';
function Navbar() {
return (
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/pages/APCSP.html">APCSP Project</a></li>
<li><a href="/pages/github-repos.html">GitHub Projects</a></li>
</ul>
<input type="color" id="theme-color-picker" title="Choose your color" />
</nav>
);
}
const Navbar = () => {
const location = useLocation();
return (
<nav className="navbar">
<div className="nav-content">
<Link to="/" className="nav-brand">
<img src="/logo.jpg" alt="Logo" className="nav-logo" />
<span className="text-glow">EndofTimee</span>
</Link>
<div className="nav-links">
<Link
to="/"
className={`nav-link ${location.pathname === '/' ? 'active' : ''}`}
>
<Home size={20} />
<span>About</span>
</Link>
<Link
to="/projects"
className={`nav-link ${location.pathname === '/projects' ? 'active' : ''}`}
>
<Code size={20} />
<span>Projects</span>
</Link>
<Link
to="/apcsp"
className={`nav-link ${location.pathname === '/apcsp' ? 'active' : ''}`}
>
<BookOpen size={20} />
<span>APCSP</span>
</Link>
<a
href="https://twitch.tv/EndofTimee"
target="_blank"
rel="noopener noreferrer"
className="nav-link"
>
<Twitch size={20} />
<span>Stream</span>
</a>
</div>
<ThemeToggle />
</div>
</nav>
);
};
export default Navbar;

View file

@ -0,0 +1,52 @@
import { useState, useEffect } from 'react';
const useGithubRepos = () => {
const [repos, setRepos] = useState([]);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchRepos = async () => {
try {
const response = await fetch('https://api.github.com/users/EndofTimee/repos?sort=updated');
if (!response.ok) {
throw new Error('Failed to fetch repositories');
}
const data = await response.json();
// Get additional details for each repo
const repoDetails = await Promise.all(
data.map(async (repo) => {
try {
const languagesResponse = await fetch(repo.languages_url);
const languages = await languagesResponse.json();
return {
...repo,
languages: Object.keys(languages)
};
} catch (error) {
console.error(`Error fetching languages for ${repo.name}:`, error);
return {
...repo,
languages: []
};
}
})
);
setRepos(repoDetails);
} catch (err) {
setError(err.message);
console.error('Error fetching repos:', err);
} finally {
setLoading(false);
}
};
fetchRepos();
}, []);
return { repos, loading, error };
};
export default useGithubRepos;

View file

@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
const useSpotifyData = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await fetch(`${process.env.REACT_APP_WORKER_URL}/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);
setLoading(false);
}
};
fetchData();
}, []);
return { data, loading, error };
};
export default useSpotifyData;

42
src/pages/APCSPPage.js Normal file
View file

@ -0,0 +1,42 @@
import React from 'react';
import FoxCard from '../components/FoxCard';
import { Code, BookOpen, Cpu } from 'lucide-react';
const APCSPPage = () => {
return (
<div className="page-container">
<FoxCard className="header-card">
<h1 className="text-glow">AP Computer Science Principles</h1>
<p className="text-gradient">Exploring the foundations of modern computing</p>
</FoxCard>
<div className="content-grid">
<FoxCard>
<div className="flex items-center gap-4">
<Code size={24} className="text-accent-primary" />
<h2>Programming Concepts</h2>
</div>
<p>Learn the creative aspects of programming, abstractions, and algorithms</p>
</FoxCard>
<FoxCard>
<div className="flex items-center gap-4">
<Cpu size={24} className="text-accent-primary" />
<h2>Project Demo</h2>
</div>
<div className="project-demo">
<iframe
src="https://drive.google.com/file/d/1JT7nZ82QJh5NIxFVHyewRBR1MLsWohEF/preview"
width="100%"
height="400"
className="rounded-lg"
allowFullScreen
/>
</div>
</FoxCard>
</div>
</div>
);
};
export default APCSPPage;

58
src/pages/AboutPage.js Normal file
View file

@ -0,0 +1,58 @@
import React from 'react';
import FoxCard from '../components/FoxCard';
import { Heart, Gamepad2, Code, Music } from 'lucide-react';
import { SpotifyVisualizer } from '../components/SpotifyVisualizer';
const AboutPage = () => {
const calculateAge = () => {
const birthDate = new Date("2009-05-15");
const today = new Date();
let age = today.getFullYear() - birthDate.getFullYear();
const monthDiff = today.getMonth() - birthDate.getMonth();
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < birthDate.getDate())) {
age--;
}
return age;
};
return (
<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>
</FoxCard>
<div className="content-grid">
<FoxCard>
<div className="flex items-center gap-4">
<Code size={24} className="text-accent-primary" />
<h2>Tech Interests</h2>
</div>
<ul className="interest-list">
<li>Programming & Development</li>
<li>Robotics & Hardware</li>
<li>Cybersecurity</li>
</ul>
</FoxCard>
<FoxCard>
<div className="flex items-center gap-4">
<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>
</FoxCard>
<FoxCard>
<div className="flex items-center gap-4">
<Music size={24} className="text-accent-primary" />
<h2>Current Tunes</h2>
</div>
<SpotifyVisualizer />
</FoxCard>
</div>
</div>
);
};
export default AboutPage;

33
src/pages/ProjectsPage.js Normal file
View file

@ -0,0 +1,33 @@
import React from 'react';
import FoxCard from '../components/FoxCard';
import GithubRepos from '../components/GithubRepos';
import useGithubRepos from '../hooks/useGithubRepos';
import LoadingFox from '../components/LoadingFox';
const ProjectsPage = () => {
const { repos, loading, error } = useGithubRepos();
return (
<div className="page-container">
<FoxCard className="header-card">
<h1 className="text-glow">My Projects</h1>
<p className="text-gradient">Exploring code, one repo at a time</p>
</FoxCard>
{loading ? (
<LoadingFox />
) : error ? (
<FoxCard className="error-card">
<p>Oops! Something went wrong fetching the repositories.</p>
<button onClick={() => window.location.reload()} className="retry-button">
Try Again
</button>
</FoxCard>
) : (
<GithubRepos repos={repos} />
)}
</div>
);
};
export default ProjectsPage;

View file

@ -1,57 +0,0 @@
/* src/styles/LoadingAnimation.css */
.loading-container {
position: fixed;
width: 100%;
height: 100%;
background: linear-gradient(135deg, rgba(26, 11, 46, 0.9), rgba(47, 28, 84, 0.9));
backdrop-filter: blur(10px);
display: flex;
justify-content: center;
align-items: center;
z-index: 9999;
}
.loading-spinner {
width: 80px;
height: 80px;
border: 4px solid rgba(157, 78, 221, 0.1);
border-left: 4px solid #9d4edd;
border-radius: 50%;
animation: spin 1s linear infinite;
position: relative;
}
.loading-spinner::after {
content: '';
position: absolute;
width: 100%;
height: 100%;
border-radius: 50%;
border: 4px solid transparent;
border-left: 4px solid #b249f8;
animation: spin 0.5s linear infinite reverse;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-text {
position: absolute;
margin-top: 100px;
color: #ffffff;
font-size: 1.2rem;
text-transform: uppercase;
letter-spacing: 2px;
animation: glow 1.5s ease-in-out infinite alternate;
}
@keyframes glow {
from {
text-shadow: 0 0 5px #e0aaff, 0 0 10px #e0aaff, 0 0 15px #b249f8;
}
to {
text-shadow: 0 0 10px #e0aaff, 0 0 20px #e0aaff, 0 0 30px #b249f8;
}
}

View file

@ -0,0 +1,20 @@
.visualizer-container {
width: 100%;
max-width: 300px;
height: 60px;
margin: 1rem auto;
background: rgba(26, 11, 46, 0.3);
border-radius: 8px;
overflow: hidden;
}
.music-visualizer {
width: 100%;
height: 100%;
}
@keyframes glow {
0% { filter: drop-shadow(0 0 2px var(--accent-neon)); }
50% { filter: drop-shadow(0 0 8px var(--accent-neon)); }
100% { filter: drop-shadow(0 0 2px var(--accent-neon)); }
}

View file

@ -0,0 +1,12 @@
<svg width="32" height="32" viewBox="0 0 32 32" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M16 4C16 4 24 8 24 16C24 24 16 28 16 28" stroke="#9d4edd" stroke-width="2">
<animateTransform
attributeName="transform"
attributeType="XML"
type="rotate"
from="0 16 16"
to="360 16 16"
dur="1s"
repeatCount="indefinite"/>
</path>
</svg>

View file

@ -1,24 +0,0 @@
.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); }
}

77
src/styles/pages.css Normal file
View file

@ -0,0 +1,77 @@
/* Page Container */
.page-container {
max-width: 1200px;
margin: 0 auto;
padding: 2rem;
min-height: calc(100vh - 4rem);
}
/* Header Card */
.header-card {
margin-bottom: 2rem;
text-align: center;
background: var(--gradient-primary);
}
.header-card h1 {
font-size: 2.5rem;
margin-bottom: 1rem;
}
/* Content Grid */
.content-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 2rem;
}
/* Interest List */
.interest-list {
list-style: none;
padding: 0;
margin: 1rem 0;
}
.interest-list li {
padding: 0.5rem 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
.interest-list li::before {
content: "🦊";
margin-right: 0.5rem;
}
/* Project Demo */
.project-demo {
margin-top: 1rem;
border-radius: var(--border-radius-lg);
overflow: hidden;
}
/* Responsive Design */
@media (max-width: 768px) {
.page-container {
padding: 1rem;
}
.header-card h1 {
font-size: 2rem;
}
.content-grid {
grid-template-columns: 1fr;
}
}
/* Animation Classes */
.fade-in {
animation: fadeIn 0.5s ease-in;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}

View file

@ -1,23 +0,0 @@
@keyframes rollingCode {
0% { transform: translateY(0); opacity: 1; }
100% { transform: translateY(100vh); opacity: 0; }
}
body {
margin: 0;
overflow: hidden;
background: black;
color: limegreen;
font-family: monospace;
font-size: 1rem;
}
.code-line {
position: absolute;
top: -10%;
width: 100%;
white-space: nowrap;
overflow: hidden;
animation: rollingCode 5s linear infinite;
}