From fe9b0ec1d77ff9d31152dc20a82f2d5ed02ea30e Mon Sep 17 00:00:00 2001 From: Austin French Date: Sun, 23 Mar 2025 15:44:06 -0700 Subject: [PATCH 1/5] wip vite working --- App.css | 21 + App.tsx | 44 + Procfile.dev | 1 + app/controllers/frontend_controller.rb | 12 + app/helpers/frontend_helper.rb | 2 + app/views/frontend/show.html.erb | 104 + components/ApiStatus.tsx | 115 + components/BentoBox.tsx | 157 + components/CodeSnippet.tsx | 80 + components/MatrixRain.tsx | 106 + components/Navigation.tsx | 25 + components/PixelCursor.tsx | 86 + components/TerminalWindow.tsx | 171 + components/ui/accordion.tsx | 56 + components/ui/alert-dialog.tsx | 139 + components/ui/alert.tsx | 59 + components/ui/aspect-ratio.tsx | 5 + components/ui/avatar.tsx | 48 + components/ui/badge.tsx | 36 + components/ui/breadcrumb.tsx | 115 + components/ui/button.tsx | 56 + components/ui/calendar.tsx | 64 + components/ui/card.tsx | 79 + components/ui/carousel.tsx | 260 + components/ui/chart.tsx | 363 ++ components/ui/checkbox.tsx | 28 + components/ui/collapsible.tsx | 9 + components/ui/command.tsx | 153 + components/ui/context-menu.tsx | 198 + components/ui/dialog.tsx | 120 + components/ui/drawer.tsx | 116 + components/ui/dropdown-menu.tsx | 198 + components/ui/form.tsx | 176 + components/ui/hover-card.tsx | 27 + components/ui/input-otp.tsx | 69 + components/ui/input.tsx | 22 + components/ui/label.tsx | 24 + components/ui/menubar.tsx | 234 + components/ui/navigation-menu.tsx | 128 + components/ui/pagination.tsx | 117 + components/ui/popover.tsx | 29 + components/ui/progress.tsx | 26 + components/ui/radio-group.tsx | 42 + components/ui/resizable.tsx | 43 + components/ui/scroll-area.tsx | 46 + components/ui/select.tsx | 158 + components/ui/separator.tsx | 29 + components/ui/sheet.tsx | 131 + components/ui/sidebar.tsx | 761 +++ components/ui/skeleton.tsx | 15 + components/ui/slider.tsx | 26 + components/ui/sonner.tsx | 29 + components/ui/switch.tsx | 27 + components/ui/table.tsx | 117 + components/ui/tabs.tsx | 53 + components/ui/textarea.tsx | 24 + components/ui/toast.tsx | 127 + components/ui/toaster.tsx | 33 + components/ui/toggle-group.tsx | 59 + components/ui/toggle.tsx | 43 + components/ui/tooltip.tsx | 28 + components/ui/use-toast.ts | 3 + config/routes.rb | 2 + hooks/use-mobile.tsx | 19 + hooks/use-toast.ts | 191 + index.css | 6 + lib/utils.ts | 6 + main.tsx | 64 + package-lock.json | 5982 +++++++++++++++--- package.json | 66 +- pages/Blog.tsx | 80 + pages/Contact.tsx | 127 + pages/Fun.tsx | 219 + pages/Games.tsx | 172 + pages/Index.tsx | 162 + pages/NotFound.tsx | 27 + pages/Work.tsx | 88 + public/favicon.ico | Bin 0 -> 1150 bytes public/placeholder.svg | 1 + public/robots.txt | 15 +- tailwind.config.ts | 181 + test/controllers/frontend_controller_test.rb | 7 + tsconfig.json | 26 + tsconfig.node.json | 10 + vite-env.d.ts | 1 + vite.config.ts | 33 + yarn.lock | 2164 ++++++- 87 files changed, 14418 insertions(+), 903 deletions(-) create mode 100644 App.css create mode 100644 App.tsx create mode 100644 app/controllers/frontend_controller.rb create mode 100644 app/helpers/frontend_helper.rb create mode 100644 app/views/frontend/show.html.erb create mode 100644 components/ApiStatus.tsx create mode 100644 components/BentoBox.tsx create mode 100644 components/CodeSnippet.tsx create mode 100644 components/MatrixRain.tsx create mode 100644 components/Navigation.tsx create mode 100644 components/PixelCursor.tsx create mode 100644 components/TerminalWindow.tsx create mode 100644 components/ui/accordion.tsx create mode 100644 components/ui/alert-dialog.tsx create mode 100644 components/ui/alert.tsx create mode 100644 components/ui/aspect-ratio.tsx create mode 100644 components/ui/avatar.tsx create mode 100644 components/ui/badge.tsx create mode 100644 components/ui/breadcrumb.tsx create mode 100644 components/ui/button.tsx create mode 100644 components/ui/calendar.tsx create mode 100644 components/ui/card.tsx create mode 100644 components/ui/carousel.tsx create mode 100644 components/ui/chart.tsx create mode 100644 components/ui/checkbox.tsx create mode 100644 components/ui/collapsible.tsx create mode 100644 components/ui/command.tsx create mode 100644 components/ui/context-menu.tsx create mode 100644 components/ui/dialog.tsx create mode 100644 components/ui/drawer.tsx create mode 100644 components/ui/dropdown-menu.tsx create mode 100644 components/ui/form.tsx create mode 100644 components/ui/hover-card.tsx create mode 100644 components/ui/input-otp.tsx create mode 100644 components/ui/input.tsx create mode 100644 components/ui/label.tsx create mode 100644 components/ui/menubar.tsx create mode 100644 components/ui/navigation-menu.tsx create mode 100644 components/ui/pagination.tsx create mode 100644 components/ui/popover.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/radio-group.tsx create mode 100644 components/ui/resizable.tsx create mode 100644 components/ui/scroll-area.tsx create mode 100644 components/ui/select.tsx create mode 100644 components/ui/separator.tsx create mode 100644 components/ui/sheet.tsx create mode 100644 components/ui/sidebar.tsx create mode 100644 components/ui/skeleton.tsx create mode 100644 components/ui/slider.tsx create mode 100644 components/ui/sonner.tsx create mode 100644 components/ui/switch.tsx create mode 100644 components/ui/table.tsx create mode 100644 components/ui/tabs.tsx create mode 100644 components/ui/textarea.tsx create mode 100644 components/ui/toast.tsx create mode 100644 components/ui/toaster.tsx create mode 100644 components/ui/toggle-group.tsx create mode 100644 components/ui/toggle.tsx create mode 100644 components/ui/tooltip.tsx create mode 100644 components/ui/use-toast.ts create mode 100644 hooks/use-mobile.tsx create mode 100644 hooks/use-toast.ts create mode 100644 index.css create mode 100644 lib/utils.ts create mode 100644 main.tsx create mode 100644 pages/Blog.tsx create mode 100644 pages/Contact.tsx create mode 100644 pages/Fun.tsx create mode 100644 pages/Games.tsx create mode 100644 pages/Index.tsx create mode 100644 pages/NotFound.tsx create mode 100644 pages/Work.tsx create mode 100644 public/favicon.ico create mode 100644 public/placeholder.svg create mode 100644 tailwind.config.ts create mode 100644 test/controllers/frontend_controller_test.rb create mode 100644 tsconfig.json create mode 100644 tsconfig.node.json create mode 100644 vite-env.d.ts create mode 100644 vite.config.ts diff --git a/App.css b/App.css new file mode 100644 index 0000000..b6915e4 --- /dev/null +++ b/App.css @@ -0,0 +1,21 @@ +/* Basic styling for the app */ +body { + background-color: #121212; + color: #ffffff; + font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; + margin: 0; + padding: 0; +} + +a { + color: #bb86fc; + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +.app-container { + min-height: 100vh; +} \ No newline at end of file diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..40774ff --- /dev/null +++ b/App.tsx @@ -0,0 +1,44 @@ +import React from 'react' +import './App.css' + +const App: React.FC = () => { + return ( +
+
+

New Frontend at /new

+
+ + + +
+

Welcome to the New Frontend

+

+ This is a simple React application served at the /new route. + If you can see this, the setup is working correctly! +

+
+
+ ) +} + +export default App \ No newline at end of file diff --git a/Procfile.dev b/Procfile.dev index da151fe..01873d3 100644 --- a/Procfile.dev +++ b/Procfile.dev @@ -1,2 +1,3 @@ web: bin/rails server css: bin/rails tailwindcss:watch +vite: npm run dev diff --git a/app/controllers/frontend_controller.rb b/app/controllers/frontend_controller.rb new file mode 100644 index 0000000..5e78bb5 --- /dev/null +++ b/app/controllers/frontend_controller.rb @@ -0,0 +1,12 @@ +class FrontendController < ApplicationController + # Disable layout for this controller since we're rendering a React app + layout false + + # Turn off content security policy for development to allow Vite HMR + content_security_policy false if Rails.env.development? + + def show + # Render the view that includes our React application + render :show + end +end \ No newline at end of file diff --git a/app/helpers/frontend_helper.rb b/app/helpers/frontend_helper.rb new file mode 100644 index 0000000..6035a51 --- /dev/null +++ b/app/helpers/frontend_helper.rb @@ -0,0 +1,2 @@ +module FrontendHelper +end diff --git a/app/views/frontend/show.html.erb b/app/views/frontend/show.html.erb new file mode 100644 index 0000000..2b79277 --- /dev/null +++ b/app/views/frontend/show.html.erb @@ -0,0 +1,104 @@ + + + + New Frontend + + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + + + + + +
+ +
+
+

New Frontend at /new

+
+ + + +
+

Welcome to the New Frontend

+

+ This is a simple React application served at the /new route. + If you can see this, the setup is working correctly! +

+
+
+
+ + + + \ No newline at end of file diff --git a/components/ApiStatus.tsx b/components/ApiStatus.tsx new file mode 100644 index 0000000..484d943 --- /dev/null +++ b/components/ApiStatus.tsx @@ -0,0 +1,115 @@ + +import { useState, useEffect } from 'react'; +import { Server, Database, Zap, GitMerge } from 'lucide-react'; + +type StatusType = 'online' | 'offline' | 'warning'; + +type Service = { + name: string; + status: StatusType; + latency: number; + icon: React.ReactNode; + message?: string; +}; + +const ApiStatus = () => { + const [services, setServices] = useState([ + { + name: 'API', + status: 'online', + latency: 42, + icon: , + message: '200 OK' + }, + { + name: 'Database', + status: 'online', + latency: 82, + icon: , + message: 'Connected' + }, + { + name: 'Server', + status: 'online', + latency: 5, + icon: , + message: 'Running' + }, + { + name: 'CI/CD', + status: 'warning', + latency: 0, + icon: , + message: 'Build in progress' + } + ]); + + // Simulate random latency changes + useEffect(() => { + const interval = setInterval(() => { + setServices(prev => + prev.map(service => { + const latencyChange = Math.floor(Math.random() * 30) - 10; + const newLatency = Math.max(1, service.latency + latencyChange); + + let status: StatusType = 'online'; + if (newLatency > 200) status = 'warning'; + if (newLatency > 500) status = 'offline'; + + // Don't change CI/CD status + if (service.name === 'CI/CD') return service; + + return { + ...service, + latency: newLatency, + status + }; + }) + ); + }, 5000); + + return () => clearInterval(interval); + }, []); + + return ( +
+
// systems status
+
+ {services.map((service) => ( +
+
+ {service.icon} + {service.name} +
+
+ + {service.message} + +
+
+
+ ))} +
+ +
+
+ uptime: + 99.98% +
+
+ last incident: + 14d ago +
+
+
+ ); +}; + +export default ApiStatus; diff --git a/components/BentoBox.tsx b/components/BentoBox.tsx new file mode 100644 index 0000000..bb68c53 --- /dev/null +++ b/components/BentoBox.tsx @@ -0,0 +1,157 @@ + +import { ReactNode } from 'react'; +import { Link } from 'react-router-dom'; + +type BentoBoxProps = { + children: ReactNode; + className?: string; + heading?: string; + subheading?: string; + to?: string; + size?: 'sm' | 'md' | 'lg' | 'xl'; + gradient?: boolean; + terminal?: boolean; + status?: 'online' | 'offline' | 'warning' | null; + variant?: 'default' | 'rails' | 'postgres' | 'code'; + animation?: 'none' | 'vu-meter' | 'loading-bar' | 'sine-wave' | 'pulse'; +}; + +const BentoBox = ({ + children, + className = "", + heading, + subheading, + to, + size = 'md', + gradient = false, + terminal = false, + status = null, + variant = 'default', + animation = 'none' +}: BentoBoxProps) => { + + const sizeClasses = { + sm: 'col-span-1 row-span-1', + md: 'col-span-1 row-span-2', + lg: 'col-span-2 row-span-1', + xl: 'col-span-2 row-span-2', + }; + + const statusColors = { + online: 'bg-terminal-success', + offline: 'bg-terminal-error', + warning: 'bg-terminal-warning' + }; + + const variantClasses = { + default: 'border-gray-700', + rails: 'card-rails', + postgres: 'card-postgres', + code: 'border-neon-green/30' + }; + + // Different animation styles + const renderAnimation = () => { + switch(animation) { + case 'vu-meter': + return
; + case 'loading-bar': + return
; + case 'sine-wave': + return
; + case 'pulse': + return
; + default: + return null; + } + }; + + const content = ( +
+ {/* Noise overlay */} +
+ + {/* Terminal title bar */} + {terminal && ( +
+
+
+
+
+
+
+ {heading ? heading : 'terminal'} +
+
+ )} + + {/* Status indicator */} + {status && ( +
+ {status === 'online' ? 'ONLINE' : status === 'offline' ? 'OFFLINE' : 'WARNING'} +
+
+ )} + + {/* Animation container at the bottom */} +
+ {renderAnimation()} +
+ + {/* Content */} +
+ {!terminal && heading && ( +

+ {heading} + {variant === 'rails' && _} + {variant === 'postgres' && _} + {variant === 'code' && _} +

+ )} + {subheading && ( +

{subheading}

+ )} +
{children}
+
+ + {/* Pixel corner decoration */} +
+
+ ); + + if (to) { + return ( + + {content} + + ); + } + + return content; +}; + +export default BentoBox; diff --git a/components/CodeSnippet.tsx b/components/CodeSnippet.tsx new file mode 100644 index 0000000..876b90a --- /dev/null +++ b/components/CodeSnippet.tsx @@ -0,0 +1,80 @@ + +import { ReactNode } from 'react'; + +type CodeSnippetProps = { + children?: ReactNode; + language?: string; + customCode?: boolean; + theme?: 'rails' | 'postgres' | 'default'; +}; + +const CodeSnippet = ({ children, language = 'typescript', customCode = false, theme = 'default' }: CodeSnippetProps) => { + const themeClasses = { + rails: 'bg-[#1c1c1c] border-[#CC0000]/30', + postgres: 'bg-[#1c1c1c] border-[#336791]/30', + default: 'bg-[#1c1c1c] border-[#39ff14]/30' + }; + + const keywordColor = theme === 'rails' ? 'text-[#CC0000]' : + theme === 'postgres' ? 'text-[#336791]' : + 'text-[#569cd6]'; + + const stringColor = theme === 'rails' ? 'text-[#ff9787]' : + theme === 'postgres' ? 'text-[#66c2ff]' : + 'text-[#ce9178]'; + + const commentColor = theme === 'rails' ? 'text-[#888888]' : + theme === 'postgres' ? 'text-[#888888]' : + 'text-[#6a9955]'; + + return ( +
+ {/* Line numbers column */} +
+
+ {[1, 2, 3, 4, 5, 6, 7, 8, 9].map(num => ( +
{num}
+ ))} +
+ + {/* Code content */} +
+ {!customCode ? ( +
+              
+                interface Skills {"{"}
+                
+ {" "}backend: 'Node.js | Ruby on Rails'; +
+ {" "}databases: 'MongoDB, PostgreSQL'; +
+ {" "}devops: 'Docker, AWS, CI/CD'; +
+ {" "}architecture: 'Microservices, Serverless'; +
+ {" "}languages: 'TypeScript, Ruby, Go'; +
+ {" "}tools: 'Git, Kubernetes, Terraform'; +
+ {"}"} +
+ // Constantly learning and improving... +
+
+ ) : ( + children + )} +
+
+ + {/* Editor status bar */} +
+
ln 9, col 42
+
UTF-8
+
{language.toUpperCase()}
+
+
+ ); +}; + +export default CodeSnippet; diff --git a/components/MatrixRain.tsx b/components/MatrixRain.tsx new file mode 100644 index 0000000..b638a03 --- /dev/null +++ b/components/MatrixRain.tsx @@ -0,0 +1,106 @@ + +import { useEffect, useRef } from 'react'; + +type MatrixRainProps = { + intensity?: 'light' | 'medium' | 'heavy'; + color?: string; +}; + +const MatrixRain = ({ intensity = 'light', color = '#39ff14' }: MatrixRainProps) => { + const canvasRef = useRef(null); + + useEffect(() => { + const canvas = canvasRef.current; + if (!canvas) return; + + const ctx = canvas.getContext('2d'); + if (!ctx) return; + + // Set canvas dimensions + const setCanvasDimensions = () => { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + }; + + setCanvasDimensions(); + window.addEventListener('resize', setCanvasDimensions); + + // Define matrix characters + const characters = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789$+-*/=%"\'#&_(),.;:?!\\|{}<>[]^~'; + const techTerms = ['rails', 'ruby', 'postgres', 'sql', 'node', 'api', 'db', 'terminal']; + + // Determine drop count based on intensity + const getDensityFactor = () => { + switch (intensity) { + case 'light': return 0.01; + case 'medium': return 0.02; + case 'heavy': return 0.05; + default: return 0.01; + } + }; + + const fontSize = 12; + const columns = Math.floor(canvas.width / fontSize); + const densityFactor = getDensityFactor(); + const drops: number[] = Array(columns).fill(1); + const opacities: number[] = Array(columns).fill(0); + + // Animation function + const draw = () => { + ctx.fillStyle = 'rgba(18, 18, 18, 0.05)'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + + ctx.fillStyle = color; + ctx.font = `${fontSize}px JetBrains Mono`; + + // Iterate over each column + for (let i = 0; i < drops.length; i++) { + // Randomly decide whether to draw or not + if (Math.random() < densityFactor) { + // Randomly decide to draw a tech term + if (Math.random() < 0.03) { + const term = techTerms[Math.floor(Math.random() * techTerms.length)]; + ctx.fillStyle = `rgba(${color.replace(/[^\d,]/g, '')}, ${opacities[i]})`; + ctx.fillText(term, i * fontSize, drops[i] * fontSize); + } else { + // Draw random character + const char = characters[Math.floor(Math.random() * characters.length)]; + ctx.fillStyle = `rgba(${color.replace(/[^\d,]/g, '')}, ${opacities[i]})`; + ctx.fillText(char, i * fontSize, drops[i] * fontSize); + } + + // Move drops and reset when they reach bottom + if (drops[i] * fontSize > canvas.height && Math.random() > 0.975) { + drops[i] = 0; + opacities[i] = 0; + } + + // Increment drop position + drops[i]++; + + // Fade in opacity at the beginning + if (opacities[i] < 1) { + opacities[i] += 0.02; + } + } + } + }; + + // Set animation interval + const interval = setInterval(draw, 50); + + return () => { + clearInterval(interval); + window.removeEventListener('resize', setCanvasDimensions); + }; + }, [intensity, color]); + + return ( + + ); +}; + +export default MatrixRain; diff --git a/components/Navigation.tsx b/components/Navigation.tsx new file mode 100644 index 0000000..da303d2 --- /dev/null +++ b/components/Navigation.tsx @@ -0,0 +1,25 @@ + +import React from 'react'; +import { Link } from 'react-router-dom'; + +const Navigation = () => { + return ( + + ); +}; + +export default Navigation; diff --git a/components/PixelCursor.tsx b/components/PixelCursor.tsx new file mode 100644 index 0000000..5b25b3b --- /dev/null +++ b/components/PixelCursor.tsx @@ -0,0 +1,86 @@ + +import { useEffect, useState } from 'react'; + +const PixelCursor = () => { + const [position, setPosition] = useState({ x: 0, y: 0 }); + const [isClicking, setIsClicking] = useState(false); + const [trail, setTrail] = useState<{x: number, y: number, opacity: number}[]>([]); + + useEffect(() => { + // Hide default cursor + document.body.style.cursor = 'none'; + + const updatePosition = (e: MouseEvent) => { + setPosition({ x: e.clientX, y: e.clientY }); + + // Add to trail with decreasing opacity + setTrail(prev => { + const newTrail = [...prev, { x: e.clientX, y: e.clientY, opacity: 0.7 }]; + if (newTrail.length > 5) { + return newTrail.slice(1).map((point, index) => ({ + ...point, + opacity: 0.7 - (index * 0.12) // Decrease opacity for older points + })); + } + return newTrail; + }); + }; + + const handleMouseDown = () => setIsClicking(true); + const handleMouseUp = () => setIsClicking(false); + + window.addEventListener('mousemove', updatePosition); + window.addEventListener('mousedown', handleMouseDown); + window.addEventListener('mouseup', handleMouseUp); + + return () => { + document.body.style.cursor = 'auto'; + window.removeEventListener('mousemove', updatePosition); + window.removeEventListener('mousedown', handleMouseDown); + window.removeEventListener('mouseup', handleMouseUp); + }; + }, []); + + return ( + <> + {/* Cursor trail */} + {trail.map((point, index) => ( +
+ ))} + + {/* Main cursor */} +
+
+ {/* Tech-themed pixelated cursor */} +
+
+
+ + ); +}; + +export default PixelCursor; diff --git a/components/TerminalWindow.tsx b/components/TerminalWindow.tsx new file mode 100644 index 0000000..2347bdd --- /dev/null +++ b/components/TerminalWindow.tsx @@ -0,0 +1,171 @@ + +import { useState, useEffect, useRef } from 'react'; + +type CommandHistoryItem = { + command: string; + responses?: string[]; + timestamp?: string; +}; + +type TerminalWindowProps = { + commands?: string[]; + responses?: string[][]; + prompt?: string; + theme?: 'rails' | 'postgres' | 'default' | 'bash'; + autoType?: boolean; + autoScroll?: boolean; + showTimestamps?: boolean; + height?: string; +}; + +const TerminalWindow = ({ + commands = [], + responses = [], + prompt = "$ ", + theme = 'default', + autoType = true, + autoScroll = true, + showTimestamps = true, + height = "100%" +}: TerminalWindowProps) => { + const [history, setHistory] = useState([]); + const [currentCommand, setCurrentCommand] = useState(""); + const [typeIndex, setTypeIndex] = useState(0); + const [commandIndex, setCommandIndex] = useState(0); + const outputRef = useRef(null); + + // Theme settings + const themeColors = { + rails: { + prompt: "#CC0000", + text: "#d0d0d0", + background: "#1a1a1a", + success: "#32CD32" + }, + postgres: { + prompt: "#336791", + text: "#d0d0d0", + background: "#1a1a1a", + success: "#32CD32" + }, + default: { + prompt: "#39ff14", + text: "#d0d0d0", + background: "#1a1a1a", + success: "#32CD32" + }, + bash: { + prompt: "#FFFFFF", + text: "#d0d0d0", + background: "#1a1a1a", + success: "#32CD32" + } + }; + + const currentTheme = themeColors[theme]; + + // Generate a timestamp in terminal format + const generateTimestamp = () => { + const now = new Date(); + const hours = now.getHours().toString().padStart(2, '0'); + const minutes = now.getMinutes().toString().padStart(2, '0'); + const seconds = now.getSeconds().toString().padStart(2, '0'); + return `[${hours}:${minutes}:${seconds}]`; + }; + + // Add a command to the history + const addCommandToHistory = (cmd: string, resp?: string[]) => { + setHistory(prev => [ + ...prev, + { + command: cmd, + responses: resp || ["Command executed successfully"], + timestamp: showTimestamps ? generateTimestamp() : undefined + } + ]); + }; + + // Simulate typing + useEffect(() => { + if (!autoType || commands.length === 0 || commandIndex >= commands.length) return; + + const currentCmd = commands[commandIndex]; + + if (typeIndex < currentCmd.length) { + const typingTimer = setTimeout(() => { + setCurrentCommand(prev => prev + currentCmd[typeIndex]); + setTypeIndex(prev => prev + 1); + }, Math.random() * 50 + 30); // Random typing speed for realism + + return () => clearTimeout(typingTimer); + } else { + // Finished typing the command + const commandResponses = responses[commandIndex] || ["Command executed successfully"]; + + const executionTimer = setTimeout(() => { + addCommandToHistory(currentCmd, commandResponses); + setCurrentCommand(""); + setTypeIndex(0); + setCommandIndex(prev => prev + 1); + }, 500); // Wait before executing + + return () => clearTimeout(executionTimer); + } + }, [autoType, commands, commandIndex, typeIndex, responses]); + + // Auto-scroll to bottom + useEffect(() => { + if (outputRef.current) { + outputRef.current.scrollTop = outputRef.current.scrollHeight; + } + }, [history, currentCommand]); + + return ( +
+
+ {history.map((item, i) => ( +
+
+ {prompt} + {item.command} +
+ + {item.responses && item.responses.map((response, j) => ( +
+ {response} +
+ ))} + + {item.timestamp && ( +
{item.timestamp}
+ )} +
+ ))} + + {/* Current typing command */} + {currentCommand && ( +
+
+ {prompt} + + {currentCommand} + +
+
+ )} +
+ + {/* Terminal status bar */} +
+
session: {theme === 'rails' ? 'rails' : theme === 'postgres' ? 'psql' : 'bash'}
+
utf-8
+
exit: ctrl+d
+
+
+ ); +}; + +export default TerminalWindow; diff --git a/components/ui/accordion.tsx b/components/ui/accordion.tsx new file mode 100644 index 0000000..e6a723d --- /dev/null +++ b/components/ui/accordion.tsx @@ -0,0 +1,56 @@ +import * as React from "react" +import * as AccordionPrimitive from "@radix-ui/react-accordion" +import { ChevronDown } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Accordion = AccordionPrimitive.Root + +const AccordionItem = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AccordionItem.displayName = "AccordionItem" + +const AccordionTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + +)) +AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName + +const AccordionContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, children, ...props }, ref) => ( + +
{children}
+
+)) + +AccordionContent.displayName = AccordionPrimitive.Content.displayName + +export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/components/ui/alert-dialog.tsx b/components/ui/alert-dialog.tsx new file mode 100644 index 0000000..8722561 --- /dev/null +++ b/components/ui/alert-dialog.tsx @@ -0,0 +1,139 @@ +import * as React from "react" +import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" + +import { cn } from "@/lib/utils" +import { buttonVariants } from "@/components/ui/button" + +const AlertDialog = AlertDialogPrimitive.Root + +const AlertDialogTrigger = AlertDialogPrimitive.Trigger + +const AlertDialogPortal = AlertDialogPrimitive.Portal + +const AlertDialogOverlay = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName + +const AlertDialogContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + + +)) +AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + +const AlertDialogHeader = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogHeader.displayName = "AlertDialogHeader" + +const AlertDialogFooter = ({ + className, + ...props +}: React.HTMLAttributes) => ( +
+) +AlertDialogFooter.displayName = "AlertDialogFooter" + +const AlertDialogTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName + +const AlertDialogDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogDescription.displayName = + AlertDialogPrimitive.Description.displayName + +const AlertDialogAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName + +const AlertDialogCancel = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName + +export { + AlertDialog, + AlertDialogPortal, + AlertDialogOverlay, + AlertDialogTrigger, + AlertDialogContent, + AlertDialogHeader, + AlertDialogFooter, + AlertDialogTitle, + AlertDialogDescription, + AlertDialogAction, + AlertDialogCancel, +} diff --git a/components/ui/alert.tsx b/components/ui/alert.tsx new file mode 100644 index 0000000..41fa7e0 --- /dev/null +++ b/components/ui/alert.tsx @@ -0,0 +1,59 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const alertVariants = cva( + "relative w-full rounded-lg border p-4 [&>svg~*]:pl-7 [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground", + { + variants: { + variant: { + default: "bg-background text-foreground", + destructive: + "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Alert = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes & VariantProps +>(({ className, variant, ...props }, ref) => ( +
+)) +Alert.displayName = "Alert" + +const AlertTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertTitle.displayName = "AlertTitle" + +const AlertDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +AlertDescription.displayName = "AlertDescription" + +export { Alert, AlertTitle, AlertDescription } diff --git a/components/ui/aspect-ratio.tsx b/components/ui/aspect-ratio.tsx new file mode 100644 index 0000000..c4abbf3 --- /dev/null +++ b/components/ui/aspect-ratio.tsx @@ -0,0 +1,5 @@ +import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" + +const AspectRatio = AspectRatioPrimitive.Root + +export { AspectRatio } diff --git a/components/ui/avatar.tsx b/components/ui/avatar.tsx new file mode 100644 index 0000000..991f56e --- /dev/null +++ b/components/ui/avatar.tsx @@ -0,0 +1,48 @@ +import * as React from "react" +import * as AvatarPrimitive from "@radix-ui/react-avatar" + +import { cn } from "@/lib/utils" + +const Avatar = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +Avatar.displayName = AvatarPrimitive.Root.displayName + +const AvatarImage = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarImage.displayName = AvatarPrimitive.Image.displayName + +const AvatarFallback = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName + +export { Avatar, AvatarImage, AvatarFallback } diff --git a/components/ui/badge.tsx b/components/ui/badge.tsx new file mode 100644 index 0000000..f000e3e --- /dev/null +++ b/components/ui/badge.tsx @@ -0,0 +1,36 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" + +import { cn } from "@/lib/utils" + +const badgeVariants = cva( + "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + { + variants: { + variant: { + default: + "border-transparent bg-primary text-primary-foreground hover:bg-primary/80", + secondary: + "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + destructive: + "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80", + outline: "text-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +export interface BadgeProps + extends React.HTMLAttributes, + VariantProps {} + +function Badge({ className, variant, ...props }: BadgeProps) { + return ( +
+ ) +} + +export { Badge, badgeVariants } diff --git a/components/ui/breadcrumb.tsx b/components/ui/breadcrumb.tsx new file mode 100644 index 0000000..71a5c32 --- /dev/null +++ b/components/ui/breadcrumb.tsx @@ -0,0 +1,115 @@ +import * as React from "react" +import { Slot } from "@radix-ui/react-slot" +import { ChevronRight, MoreHorizontal } from "lucide-react" + +import { cn } from "@/lib/utils" + +const Breadcrumb = React.forwardRef< + HTMLElement, + React.ComponentPropsWithoutRef<"nav"> & { + separator?: React.ReactNode + } +>(({ ...props }, ref) => +
); }; diff --git a/components/TerminalWindow.tsx b/components/TerminalWindow.tsx index 2347bdd..f5d83e9 100644 --- a/components/TerminalWindow.tsx +++ b/components/TerminalWindow.tsx @@ -121,10 +121,21 @@ const TerminalWindow = ({ }, [history, currentCommand]); return ( -
+
+ {/* Terminal header with macOS buttons */} +
+
+
+
+
+
+
+ {theme === 'rails' ? 'rails console' : theme === 'postgres' ? 'psql shell' : 'bash'} +
+
{history.map((item, i) => (
@@ -159,7 +170,7 @@ const TerminalWindow = ({
{/* Terminal status bar */} -
+
session: {theme === 'rails' ? 'rails' : theme === 'postgres' ? 'psql' : 'bash'}
utf-8
exit: ctrl+d
diff --git a/components/WorkCard.tsx b/components/WorkCard.tsx new file mode 100644 index 0000000..d69cfdf --- /dev/null +++ b/components/WorkCard.tsx @@ -0,0 +1,42 @@ +import { FC } from 'react'; + +export interface WorkCardProps { + id: number; + title: string; + description: string; + tags: string[]; + image: string; +} + +const WorkCard: FC = ({ title, description, tags, image }) => { + return ( +
+
+ {title} +
+
+
+

{title}

+

{description}

+
+ {tags.map((tag) => ( + + {tag} + + ))} +
+
+ +
+
+
+ ); +}; + +export default WorkCard; \ No newline at end of file diff --git a/components/WorkGrid.tsx b/components/WorkGrid.tsx new file mode 100644 index 0000000..4b08436 --- /dev/null +++ b/components/WorkGrid.tsx @@ -0,0 +1,30 @@ +import { FC } from 'react'; +import WorkCard, { WorkCardProps } from './WorkCard'; + +interface WorkGridProps { + projects: WorkCardProps[]; +} + +const WorkGrid: FC = ({ projects }) => { + return ( +
+ {projects.map((project) => ( + + ))} + {projects.length === 0 && ( +
+

No projects available at the moment.

+
+ )} +
+ ); +}; + +export default WorkGrid; \ No newline at end of file diff --git a/components/ui/toaster.tsx b/components/ui/toaster.tsx index 6c67edf..77f10f2 100644 --- a/components/ui/toaster.tsx +++ b/components/ui/toaster.tsx @@ -1,4 +1,4 @@ -import { useToast } from "@/hooks/use-toast" +import { useToast } from "../../hooks/use-toast" import { Toast, ToastClose, @@ -6,7 +6,7 @@ import { ToastProvider, ToastTitle, ToastViewport, -} from "@/components/ui/toast" +} from "./toast" export function Toaster() { const { toasts } = useToast() diff --git a/components/ui/tooltip.tsx b/components/ui/tooltip.tsx index e121f0a..5503ba1 100644 --- a/components/ui/tooltip.tsx +++ b/components/ui/tooltip.tsx @@ -1,7 +1,7 @@ import * as React from "react" import * as TooltipPrimitive from "@radix-ui/react-tooltip" -import { cn } from "@/lib/utils" +import { cn } from "../../lib/utils" const TooltipProvider = TooltipPrimitive.Provider diff --git a/config/routes.rb b/config/routes.rb index b2ed725..fa7359c 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -9,16 +9,51 @@ # get "manifest" => "rails/pwa#manifest", as: :pwa_manifest # get "service-worker" => "rails/pwa#service_worker", as: :pwa_service_worker + # API endpoints + namespace :api do + resources :blog_posts, only: [:index, :show], param: :slug + resources :game_posts, only: [:index, :show], param: :slug + resources :work_posts, only: [:index, :show], param: :slug + end + # Blog routes get "/blog", to: "blog#index" get "/blog/:slug", to: "blog#show", as: :blog_post get "/blog/:slug/content", to: "blog#content", as: :blog_post_content + # Games routes + get "/games", to: "games#index" + + # Work routes + get "/works", to: "works#index" + + # Admin routes + scope :admin do + get '/', to: 'admin#dashboard', as: :admin_dashboard + + # Blog posts + get '/blog-posts', to: 'admin#blog_posts', as: :admin_blog_posts + + # Game posts + get '/game-posts', to: 'admin#game_posts', as: :admin_game_posts + get '/game-posts/new', to: 'admin#new_game_post', as: :admin_new_game_post + post '/game-posts', to: 'admin#create_game_post', as: :admin_create_game_post + get '/game-posts/:id/edit', to: 'admin#edit_game_post', as: :admin_edit_game_post + patch '/game-posts/:id', to: 'admin#update_game_post', as: :admin_update_game_post + delete '/game-posts/:id', to: 'admin#destroy_game_post', as: :admin_destroy_game_post + + # Work posts + get '/work-posts', to: 'admin#work_posts', as: :admin_work_posts + get '/work-posts/new', to: 'admin#new_work_post', as: :admin_new_work_post + post '/work-posts', to: 'admin#create_work_post', as: :admin_create_work_post + get '/work-posts/:id/edit', to: 'admin#edit_work_post', as: :admin_edit_work_post + patch '/work-posts/:id', to: 'admin#update_work_post', as: :admin_update_work_post + delete '/work-posts/:id', to: 'admin#destroy_work_post', as: :admin_destroy_work_post + end + # Portfolio routes - get "/work", to: "portfolio#work" get "/contact", to: "portfolio#contact" get "/fun", to: "portfolio#fun" - get "/games", to: "portfolio#games" get "/games/arena-shooter", to: "portfolio#arena_shooter" get "/movement-demo", to: "movement_demo#index" @@ -26,8 +61,8 @@ get "/dashboard", to: "dashboard#hello", as: :dashboard post "turbo_message", to: "dashboard#turbo_message", as: :turbo_message - get '/new', to: 'frontend#show', as: :new_frontend - get '/new/*path', to: 'frontend#show' + # get '/new', to: 'frontend#show', as: :new_frontend + # get '/new/*path', to: 'frontend#show' # Portfolio as root root "portfolio#index" end diff --git a/db/migrate/20250324000001_create_game_posts.rb b/db/migrate/20250324000001_create_game_posts.rb new file mode 100644 index 0000000..5ebdf77 --- /dev/null +++ b/db/migrate/20250324000001_create_game_posts.rb @@ -0,0 +1,15 @@ +class CreateGamePosts < ActiveRecord::Migration[7.1] + def change + create_table :game_posts do |t| + t.string :title, null: false + t.text :description, null: false + t.string :slug, null: false + t.string :image_url, null: false + t.string :link, null: false + t.boolean :featured, default: false + + t.timestamps + end + add_index :game_posts, :slug, unique: true + end +end \ No newline at end of file diff --git a/db/migrate/20250324000002_create_work_posts.rb b/db/migrate/20250324000002_create_work_posts.rb new file mode 100644 index 0000000..dbad40b --- /dev/null +++ b/db/migrate/20250324000002_create_work_posts.rb @@ -0,0 +1,15 @@ +class CreateWorkPosts < ActiveRecord::Migration[7.1] + def change + create_table :work_posts do |t| + t.string :title, null: false + t.text :description, null: false + t.string :slug, null: false + t.string :image_url, null: false + t.string :tags, null: false, array: true, default: [] + t.boolean :featured, default: false + + t.timestamps + end + add_index :work_posts, :slug, unique: true + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 6897e55..2c6d6f7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_03_09_000001) do +ActiveRecord::Schema[8.0].define(version: 2025_03_24_000002) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -26,6 +26,18 @@ t.index ["slug"], name: "index_blog_posts_on_slug", unique: true end + create_table "game_posts", force: :cascade do |t| + t.string "title", null: false + t.text "description", null: false + t.string "slug", null: false + t.string "image_url", null: false + t.string "link", null: false + t.boolean "featured", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["slug"], name: "index_game_posts_on_slug", unique: true + end + create_table "service_statuses", force: :cascade do |t| t.string "name" t.string "status" @@ -42,4 +54,16 @@ t.datetime "created_at", null: false t.datetime "updated_at", null: false end + + create_table "work_posts", force: :cascade do |t| + t.string "title", null: false + t.text "description", null: false + t.string "slug", null: false + t.string "image_url", null: false + t.string "tags", default: [], null: false, array: true + t.boolean "featured", default: false + t.datetime "created_at", null: false + t.datetime "updated_at", null: false + t.index ["slug"], name: "index_work_posts_on_slug", unique: true + end end diff --git a/db/seeds.rb b/db/seeds.rb index bff2093..fd5ac07 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -89,3 +89,85 @@ def self.published MARKDOWN post.published_at = Time.current end + +# Create sample game posts +puts "Creating game posts..." +[ + { + title: "Arena Shooter", + description: "Fast-paced 2D arena shooter with enemies, power-ups, and high scores.", + image_url: "/assets/images/games/arena-shooter.jpg", + link: "/portfolio/arena_shooter", + featured: true + }, + { + title: "Platformer", + description: "Classic side-scrolling platformer with collectibles and obstacles.", + image_url: "/assets/images/games/platformer.jpg", + link: "#", + featured: false + }, + { + title: "Puzzle Game", + description: "Brain-teasing puzzle game with multiple levels of difficulty.", + image_url: "/assets/images/games/puzzle.jpg", + link: "#", + featured: false + }, + { + title: "RPG Adventure", + description: "Story-driven RPG with character progression and turn-based combat.", + image_url: "/assets/images/games/rpg.jpg", + link: "#", + featured: false + } +].each do |game_data| + GamePost.find_or_create_by!(title: game_data[:title]) do |game| + game.description = game_data[:description] + game.image_url = game_data[:image_url] + game.link = game_data[:link] + game.featured = game_data[:featured] + end +end + +# Create sample work posts +puts "Creating work posts..." +[ + { + title: "E-commerce Redesign", + description: "A complete overhaul of an online store with improved UX and conversion rates.", + image_url: "https://images.unsplash.com/photo-1487058792275-0ad4aaf24ca7", + tags: ["React", "Node.js", "Tailwind CSS"], + featured: true + }, + { + title: "Health App Dashboard", + description: "An intuitive dashboard for a health tracking application with data visualization.", + image_url: "https://images.unsplash.com/photo-1488590528505-98d2b5aba04b", + tags: ["Vue.js", "D3.js", "Firebase"], + featured: true + }, + { + title: "Financial Analytics Platform", + description: "A comprehensive platform for financial data analysis and reporting.", + image_url: "https://images.unsplash.com/photo-1526374965328-7f61d4dc18c5", + tags: ["TypeScript", "React", "MongoDB"], + featured: false + }, + { + title: "Travel Blog", + description: "A responsive travel blog with dynamic content and interactive maps.", + image_url: "https://images.unsplash.com/photo-1649972904349-6e44c42644a7", + tags: ["WordPress", "PHP", "JavaScript"], + featured: false + } +].each do |work_data| + WorkPost.find_or_create_by!(title: work_data[:title]) do |work| + work.description = work_data[:description] + work.image_url = work_data[:image_url] + work.tags = work_data[:tags] + work.featured = work_data[:featured] + end +end + +puts "Seed data created successfully!" diff --git a/index.css b/index.css index e2ad398..ad83e5d 100644 --- a/index.css +++ b/index.css @@ -220,15 +220,15 @@ @apply bg-terminal-warning; } - /* Matrix rain effect */ + /* Matrix rain effect - disabled */ @keyframes matrix-rain { 0% { - opacity: 1; - transform: translateY(-100%); + opacity: 0; + transform: translateY(0); } 100% { opacity: 0; - transform: translateY(100%); + transform: translateY(0); } } diff --git a/package.json b/package.json index 2f77c61..4559b56 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "@radix-ui/react-toast": "^1.2.1", "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", - "@radix-ui/react-tooltip": "^1.1.4", + "@radix-ui/react-tooltip": "^1.1.8", "@react-three/drei": "^10.0.4", "@react-three/fiber": "^9.1.0", "@tanstack/react-query": "^5.56.2", @@ -70,7 +70,7 @@ "date-fns": "^3.6.0", "embla-carousel-react": "^8.3.0", "input-otp": "^1.2.4", - "lucide-react": "^0.462.0", + "lucide-react": "^0.483.0", "next-themes": "^0.3.0", "react": "^18.3.1", "react-day-picker": "^8.10.1", @@ -85,7 +85,7 @@ "rehype-sanitize": "^6.0.0", "remark-gfm": "^4.0.1", "sonner": "^1.5.0", - "tailwind-merge": "^2.5.2", + "tailwind-merge": "^3.0.2", "tailwindcss": "^3.4.11", "tailwindcss-animate": "^1.0.7", "three": "^0.174.0", diff --git a/pages/Games.tsx b/pages/Games.tsx index 35576ce..b53a04d 100644 --- a/pages/Games.tsx +++ b/pages/Games.tsx @@ -1,7 +1,8 @@ - import { ChevronLeft } from 'lucide-react'; import { Link } from 'react-router-dom'; import { useState, useEffect } from 'react'; +import GamesGrid from '../components/GamesGrid'; +import type { Game } from '../components/GamesGrid'; // Simple Memory Game const MemoryGame = () => { @@ -137,7 +138,72 @@ const MemoryGame = () => { ); }; +// Type for API response +interface GamePost { + id: number; + title: string; + description: string; + slug: string; + image_url: string; + link: string; + featured: boolean; + created_at: string; + updated_at: string; +} + const Games = () => { + const [gameProjects, setGameProjects] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchGames = async () => { + try { + setLoading(true); + const response = await fetch('/api/game_posts.json'); + + if (!response.ok) { + throw new Error(`Error fetching games: ${response.statusText}`); + } + + const data: GamePost[] = await response.json(); + + // Transform the API data to match our Game interface + const formattedGames: Game[] = data.map(game => ({ + title: game.title, + description: game.description, + imageUrl: game.image_url, + link: game.link + })); + + setGameProjects(formattedGames); + setLoading(false); + } catch (err) { + console.error('Failed to fetch games:', err); + setError('Failed to load games. Please try again later.'); + setLoading(false); + + // Fallback data in case API fails + setGameProjects([ + { + title: "Arena Shooter", + description: "Fast-paced 2D arena shooter with enemies, power-ups, and high scores.", + imageUrl: "/assets/images/games/arena-shooter.jpg", + link: "/portfolio/arena_shooter", + }, + { + title: "Platformer", + description: "Classic side-scrolling platformer with collectibles and obstacles.", + imageUrl: "/assets/images/games/platformer.jpg", + link: "#", + } + ]); + } + }; + + fetchGames(); + }, []); + return (
@@ -148,25 +214,44 @@ const Games = () => {

Games

- Take a break and have some fun with these mini-games + Check out my game development projects and have some fun with mini-games

-
- +
+

Game Projects

-
-

More Games Coming Soon!

-
-
-
-

Stay tuned for additional mini-games...

+ {loading ? ( +
+
+
+ ) : error ? ( +
+

{error}

+
+ ) : ( + + )} +
+ +
+

Mini Games

+
+ + +
+

More Games Coming Soon!

+
+
+
+

Stay tuned for additional mini-games...

+
-
+
); }; -export default Games; +export default Games; \ No newline at end of file diff --git a/pages/Index.tsx b/pages/Index.tsx index 0d5b68c..9ea30c2 100644 --- a/pages/Index.tsx +++ b/pages/Index.tsx @@ -3,7 +3,7 @@ import { useState, useEffect } from 'react'; import BentoBox from '../components/BentoBox'; import CodeSnippet from '../components/CodeSnippet'; import TerminalWindow from '../components/TerminalWindow'; -import MatrixRain from '../components/MatrixRain'; +// MatrixRain component is removed for cleaner design import { FileCode, Database, Terminal, Wifi, Keyboard, Mic } from 'lucide-react'; const Index = () => { @@ -29,24 +29,24 @@ const Index = () => { return (
- + {/* Matrix rain removed for cleaner design */} {/* Header */} -
-
+
+
Backend Developer / System Architect
-

+

Austin French

-
+

Crafting robust backend systems and databases with clean, efficient code.

{/* Bento Layout */} -
+
{/* About Me */}

@@ -121,7 +121,7 @@ const Index = () => { {/* Contact, Fun, and Games in a row */} -

+
@@ -143,14 +143,14 @@ const Index = () => {
{/* Code snippet at the bottom - moved up */} -
+
{/* Footer */} -
); }; -export default Work; +export default Work; \ No newline at end of file