Initial changes

This commit is contained in:
Tickbase
2025-05-18 08:06:56 +02:00
parent 19087c00da
commit 0be15f83e7
82 changed files with 4636 additions and 3237 deletions

View File

@@ -0,0 +1,121 @@
import { useEffect, useRef } from 'react'
/**
* Animated background component that draws an interactive particle effect
*/
const AnimatedBackground = () => {
const canvasRef = useRef<HTMLCanvasElement>(null)
useEffect(() => {
const canvas = canvasRef.current
if (!canvas) return
const ctx = canvas.getContext('2d')
if (!ctx) return
// Set canvas size to match window
const setCanvasSize = () => {
canvas.width = window.innerWidth
canvas.height = window.innerHeight
}
setCanvasSize()
window.addEventListener('resize', setCanvasSize)
// Create particles
const particles: Particle[] = []
const particleCount = 30
interface Particle {
x: number
y: number
size: number
speedX: number
speedY: number
opacity: number
color: string
}
// Color palette matching our theme
const colors = [
'rgba(74, 118, 196, 0.5)', // primary blue
'rgba(155, 125, 255, 0.5)', // purple
'rgba(251, 177, 60, 0.5)', // gold
]
// Create initial particles
for (let i = 0; i < particleCount; i++) {
particles.push({
x: Math.random() * canvas.width,
y: Math.random() * canvas.height,
size: Math.random() * 3 + 1,
speedX: Math.random() * 0.2 - 0.1,
speedY: Math.random() * 0.2 - 0.1,
opacity: Math.random() * 0.07 + 0.03,
color: colors[Math.floor(Math.random() * colors.length)],
})
}
// Animation loop
const animate = () => {
// Clear canvas with transparent black to create fade effect
ctx.fillStyle = 'rgba(15, 15, 15, 0.1)'
ctx.fillRect(0, 0, canvas.width, canvas.height)
// Update and draw particles
particles.forEach((particle) => {
// Update position
particle.x += particle.speedX
particle.y += particle.speedY
// Wrap around edges
if (particle.x < 0) particle.x = canvas.width
if (particle.x > canvas.width) particle.x = 0
if (particle.y < 0) particle.y = canvas.height
if (particle.y > canvas.height) particle.y = 0
// Draw particle
ctx.beginPath()
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2)
ctx.fillStyle = particle.color.replace('0.5', `${particle.opacity}`)
ctx.fill()
// Connect particles that are close to each other
particles.forEach((otherParticle) => {
const dx = particle.x - otherParticle.x
const dy = particle.y - otherParticle.y
const distance = Math.sqrt(dx * dx + dy * dy)
if (distance < 100) {
ctx.beginPath()
ctx.strokeStyle = particle.color.replace('0.5', `${particle.opacity * 0.5}`)
ctx.lineWidth = 0.2
ctx.moveTo(particle.x, particle.y)
ctx.lineTo(otherParticle.x, otherParticle.y)
ctx.stroke()
}
})
})
requestAnimationFrame(animate)
}
// Start animation
animate()
// Cleanup
return () => {
window.removeEventListener('resize', setCanvasSize)
}
}, [])
return (
<canvas
ref={canvasRef}
className="animated-background"
aria-hidden="true"
/>
)
}
export default AnimatedBackground

View File

@@ -0,0 +1,81 @@
import { Component, ErrorInfo, ReactNode } from 'react'
import { Button } from '@/components/buttons'
interface ErrorBoundaryProps {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: ErrorInfo) => void;
}
interface ErrorBoundaryState {
hasError: boolean;
error: Error | null;
}
/**
* Error boundary component to catch and display runtime errors
*/
class ErrorBoundary extends Component<ErrorBoundaryProps, ErrorBoundaryState> {
constructor(props: ErrorBoundaryProps) {
super(props)
this.state = {
hasError: false,
error: null,
}
}
static getDerivedStateFromError(error: Error): ErrorBoundaryState {
// Update state so the next render will show the fallback UI
return {
hasError: true,
error,
}
}
componentDidCatch(error: Error, errorInfo: ErrorInfo): void {
// Log the error
console.error('ErrorBoundary caught an error:', error, errorInfo)
// Call the onError callback if provided
if (this.props.onError) {
this.props.onError(error, errorInfo)
}
}
handleReset = () => {
this.setState({ hasError: false, error: null })
}
render(): ReactNode {
if (this.state.hasError) {
// Use custom fallback if provided
if (this.props.fallback) {
return this.props.fallback
}
// Default error UI
return (
<div className="error-container">
<h2>Something went wrong</h2>
<details>
<summary>Error details</summary>
<p>{this.state.error?.toString()}</p>
</details>
<Button
variant="primary"
onClick={this.handleReset}
className="error-retry-button"
>
Try again
</Button>
</div>
)
}
return this.props.children
}
}
export default ErrorBoundary

View File

@@ -0,0 +1,44 @@
import { Button } from '@/components/buttons'
interface HeaderProps {
onRefresh: () => void
refreshDisabled?: boolean
onSearch: (query: string) => void
searchQuery: string
}
/**
* Application header component
* Contains the app title, search input, and refresh button
*/
const Header = ({
onRefresh,
refreshDisabled = false,
onSearch,
searchQuery,
}: HeaderProps) => {
return (
<header className="app-header">
<h1>CreamLinux</h1>
<div className="header-controls">
<Button
variant="primary"
onClick={onRefresh}
disabled={refreshDisabled}
className="refresh-button"
>
Refresh
</Button>
<input
type="text"
placeholder="Search games..."
className="search-input"
value={searchQuery}
onChange={(e) => onSearch(e.target.value)}
/>
</div>
</header>
)
}
export default Header

View File

@@ -0,0 +1,53 @@
import { useEffect } from 'react'
interface InitialLoadingScreenProps {
message: string;
progress: number;
onComplete?: () => void;
}
/**
* Initial loading screen displayed when the app first loads
*/
const InitialLoadingScreen = ({
message,
progress,
onComplete
}: InitialLoadingScreenProps) => {
// Call onComplete when progress reaches 100%
useEffect(() => {
if (progress >= 100 && onComplete) {
const timer = setTimeout(() => {
onComplete();
}, 500); // Small delay to show completion
return () => clearTimeout(timer);
}
}, [progress, onComplete]);
return (
<div className="initial-loading-screen">
<div className="loading-content">
<h1>CreamLinux</h1>
<div className="loading-animation">
<div className="loading-circles">
<div className="circle circle-1"></div>
<div className="circle circle-2"></div>
<div className="circle circle-3"></div>
</div>
</div>
<p className="loading-message">{message}</p>
<div className="progress-bar-container">
<div className="progress-bar" style={{ width: `${progress}%` }} />
</div>
<div className="progress-percentage">{Math.round(progress)}%</div>
</div>
</div>
)
}
export default InitialLoadingScreen

View File

@@ -0,0 +1,36 @@
interface SidebarProps {
setFilter: (filter: string) => void
currentFilter: string
}
/**
* Application sidebar component
* Contains filters for game types
*/
const Sidebar = ({ setFilter, currentFilter }: SidebarProps) => {
// Available filter options
const filters = [
{ id: 'all', label: 'All Games' },
{ id: 'native', label: 'Native' },
{ id: 'proton', label: 'Proton Required' }
]
return (
<div className="sidebar">
<h2>Library</h2>
<ul className="filter-list">
{filters.map(filter => (
<li
key={filter.id}
className={currentFilter === filter.id ? 'active' : ''}
onClick={() => setFilter(filter.id)}
>
{filter.label}
</li>
))}
</ul>
</div>
)
}
export default Sidebar

View File

@@ -0,0 +1,6 @@
// Export all layout components
export { default as Header } from './Header';
export { default as Sidebar } from './Sidebar';
export { default as AnimatedBackground } from './AnimatedBackground';
export { default as InitialLoadingScreen } from './InitialLoadingScreen';
export { default as ErrorBoundary } from './ErrorBoundary';