mirror of
https://github.com/Novattz/creamlinux-installer.git
synced 2026-01-29 23:02:50 -05:00
Initial changes
This commit is contained in:
121
src/components/layout/AnimatedBackground.tsx
Normal file
121
src/components/layout/AnimatedBackground.tsx
Normal 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
|
||||
81
src/components/layout/ErrorBoundary.tsx
Normal file
81
src/components/layout/ErrorBoundary.tsx
Normal 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
|
||||
44
src/components/layout/Header.tsx
Normal file
44
src/components/layout/Header.tsx
Normal 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
|
||||
53
src/components/layout/InitialLoadingScreen.tsx
Normal file
53
src/components/layout/InitialLoadingScreen.tsx
Normal 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
|
||||
36
src/components/layout/Sidebar.tsx
Normal file
36
src/components/layout/Sidebar.tsx
Normal 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
|
||||
6
src/components/layout/index.ts
Normal file
6
src/components/layout/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user