From c994c1ec97c69c450ebad7f2fa04f2beabc8d7f6 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 25 Nov 2025 15:10:49 +0800 Subject: [PATCH 01/13] feat: Create interactive footer component with particle effects - Implemented animated footer with mouse-tracking gradient effects - Added canvas-based network visualization with particle connections - Created interactive particle system with hover effects - Integrated social media links (Discord, Twitter, GitHub, YouTube) - Added navigation links (Home, About, Events, Game/Art Showcases) - Implemented community stats display with animated cards - Added newsletter signup button with rotating star animation - Created constitution link with custom island icon - Optimized code to 225 lines while maintaining all functionality - Added comprehensive comments for maintainability - Used Framer Motion for smooth animations and interactions - Implemented responsive design with proper mobile/desktop layouts - Added client-side rendering checks to prevent SSR hydration issues --- client/src/components/footer.tsx | 235 +++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 client/src/components/footer.tsx diff --git a/client/src/components/footer.tsx b/client/src/components/footer.tsx new file mode 100644 index 0000000..4da346a --- /dev/null +++ b/client/src/components/footer.tsx @@ -0,0 +1,235 @@ +"use client"; +import Link from "next/link"; +import Image from "next/image"; +import { useState, useRef, useEffect } from "react"; +import { motion, useMotionValue, useSpring, useTransform, useMotionTemplate, MotionValue } from "framer-motion"; +import { Gamepad2, Sparkles, Code2, Palette, Calendar, Users, ChevronRight, Heart, Zap, Trophy, Star } from "lucide-react"; + +// Type definitions for particle system +type ParticleConfig = { baseX: number; baseY: number; size: number; delay: number; duration: number; color?: string }; +type NetworkParticle = { x: number; y: number; vx: number; vy: number; size: number }; + +// Gradient that follows mouse cursor position for interactive effect +function MouseGradient({ smoothX, smoothY, isHovering }: { smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean }) { + const background = useMotionTemplate`radial-gradient(circle 600px at ${smoothX}% ${smoothY}%, rgba(139, 92, 246, 0.3) 0%, rgba(236, 72, 153, 0.12) 40%, transparent 70%)`; + return ; +} + +// Individual particle component with pulsing animation and mouse interaction +function SimpleParticle({ baseX, baseY, size, delay, duration, smoothX, smoothY, isHovering, color = "rgba(255, 255, 255, 0.6)" }: ParticleConfig & { smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean }) { + // Particles react to mouse movement when hovering + const offsetX = useTransform(smoothX, (mx) => isHovering ? (mx - 50) * 0.3 : 0); + const offsetY = useTransform(smoothY, (my) => isHovering ? (my - 50) * 0.3 : 0); + return + +
+ + ; +} + +// Canvas-based network visualization with particle connections +function NetworkCanvas({ width, height, smoothX, smoothY, isHovering }: { width: number; height: number; smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean }) { + const canvasRef = useRef(null); + const particlesRef = useRef([]); + const initializedRef = useRef(false); + + useEffect(() => { + // Initialize particles only once to prevent regeneration on re-render + if (!initializedRef.current) { + particlesRef.current = Array.from({ length: 22 }, () => ({ x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.5) * 0.15, size: 1.5 + Math.random() * 1.5 })); + initializedRef.current = true; + } + const canvas = canvasRef.current; + if (!canvas) return; + const ctx = canvas.getContext("2d"); + if (!ctx) return; + let id: number; + + const render = () => { + ctx.clearRect(0, 0, width, height); + const pts = particlesRef.current; + const mx = (smoothX.get() / 100) * width; // Convert percentage to pixel coordinates + const my = (smoothY.get() / 100) * height; + + // Update particle positions with boundary collision + pts.forEach((p) => { + p.x += p.vx; + p.y += p.vy; + if (p.x < 0 || p.x > width) p.vx *= -1; + if (p.y < 0 || p.y > height) p.vy *= -1; + p.x = Math.max(0, Math.min(width, p.x)); + p.y = Math.max(0, Math.min(height, p.y)); + }); + + // Draw connections between nearby particles (max 2 connections per particle) + for (let i = 0; i < pts.length; i++) { + const pA = pts[i]; + const dists = pts.slice(i + 1).map((pB, j) => { + const dx = pA.x - pB.x; + const dy = pA.y - pB.y; + return { index: i + j + 1, dist: Math.sqrt(dx * dx + dy * dy) }; + }).filter(d => d.dist < 150).sort((a, b) => a.dist - b.dist).slice(0, 2); + + dists.forEach(({ index, dist }) => { + const pB = pts[index]; + const op = (1 - dist / 150) * 0.25; // Opacity decreases with distance + const grad = ctx.createLinearGradient(pA.x, pA.y, pB.x, pB.y); + grad.addColorStop(0, `rgba(139, 92, 246, ${op})`); + grad.addColorStop(0.5, `rgba(196, 181, 253, ${op * 1.5})`); + grad.addColorStop(1, `rgba(236, 72, 153, ${op})`); + ctx.strokeStyle = grad; + ctx.lineWidth = 1.5; + ctx.beginPath(); + ctx.moveTo(pA.x, pA.y); + ctx.lineTo(pB.x, pB.y); + ctx.stroke(); + }); + } + + // Draw connections from particles to mouse cursor when hovering + if (isHovering) { + pts.forEach((p) => { + const dx = p.x - mx; + const dy = p.y - my; + const dist = Math.sqrt(dx * dx + dy * dy); + if (dist < 120) { + const op = (1 - dist / 120) * 0.4; + const grad = ctx.createLinearGradient(p.x, p.y, mx, my); + grad.addColorStop(0, `rgba(236, 72, 153, ${op})`); + grad.addColorStop(1, `rgba(255, 255, 255, ${op * 0.5})`); + ctx.strokeStyle = grad; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(p.x, p.y); + ctx.lineTo(mx, my); + ctx.stroke(); + } + }); + } + id = requestAnimationFrame(render); + }; + render(); + return () => cancelAnimationFrame(id); + }, [width, height, smoothX, smoothY, isHovering]); + return ; +} + +export function Footer() { + const footerRef = useRef(null); + const [isHovered, setIsHovered] = useState(null); // Track which link is hovered for underline effect + const [isHovering, setIsHovering] = useState(false); + const [dimensions, setDimensions] = useState({ width: 1920, height: 400 }); + const [isClient, setIsClient] = useState(false); // Prevent SSR issues with canvas/animations + const [particleConfigs, setParticleConfigs] = useState([]); + + // Mouse position tracking with spring physics for smooth movement + const mouseX = useMotionValue(50); + const mouseY = useMotionValue(50); + const smoothX = useSpring(mouseX, { damping: 50, stiffness: 100 }); + const smoothY = useSpring(mouseY, { damping: 50, stiffness: 100 }); + + // Initialize particles on client-side only (prevents hydration mismatch) + useEffect(() => { + setIsClient(true); + const colors = ["rgba(255, 255, 255, 0.5)", "rgba(196, 181, 253, 0.4)", "rgba(168, 85, 247, 0.4)", "rgba(236, 72, 153, 0.4)"]; + setParticleConfigs(Array.from({ length: 22 }, () => ({ baseX: Math.random() * 100, baseY: Math.random() * 100, size: 2 + Math.random() * 3, delay: Math.random() * 4, duration: 3 + Math.random() * 3, color: colors[Math.floor(Math.random() * colors.length)] }))); + }, []); + + // Update canvas dimensions on window resize + useEffect(() => { + const update = () => footerRef.current && setDimensions({ width: footerRef.current.offsetWidth, height: footerRef.current.offsetHeight }); + update(); + window.addEventListener('resize', update); + return () => window.removeEventListener('resize', update); + }, []); + + // Convert mouse coordinates to percentage for gradient positioning + const handleMouseMove = (e: React.MouseEvent) => { + const rect = e.currentTarget.getBoundingClientRect(); + mouseX.set(((e.clientX - rect.left) / rect.width) * 100); + mouseY.set(((e.clientY - rect.top) / rect.height) * 100); + setIsHovering(true); + }; + + const mainLinks = [{ label: "Home", href: "/", icon: }, { label: "About", href: "/about", icon: }, { label: "Events", href: "/events", icon: }, { label: "Game Showcase", href: "/game-showcase", icon: }, { label: "Art Showcase", href: "/art-showcase", icon: }]; + const socialLinks = [ + { icon: , href: "#", label: "Discord", color: "hover:text-[#5865F2]" }, + { icon: , href: "#", label: "X (Twitter)", color: "hover:text-white" }, + { icon: , href: "#", label: "GitHub", color: "hover:text-white" }, + { icon: , href: "#", label: "YouTube", color: "hover:text-[#FF0000]" }, + ]; + const quickLinks = [{ label: "Join the Club", href: "#" }, { label: "Submit Your Game", href: "#" }, { label: "Upcoming Jams", href: "#" }, { label: "Resources", href: "#" }]; + const stats = [{ label: "Active Members", value: "150+", icon: }, { label: "Games Created", value: "50+", icon: }, { label: "Events Hosted", value: "25+", icon: }]; + + return
setIsHovering(false)}> +
+ +
+ {isClient && } + {isClient &&
{particleConfigs.map((cfg, i) => )}
} +
+
+
+
+
+ + + UWA Game Development Logo + + + +
+

Game Development

+

Create • Play • Inspire

+
+
+

Building the next generation of game developers at UWA game development club

+
{socialLinks.map((social, index) => {social.icon})}
+
+
+

Quick Links

+
    {quickLinks.map((link) =>
  • setIsHovered(link.label)} onMouseLeave={() => setIsHovered(null)}>{link.label}{isHovered === link.label && }
  • )}
+
+
+

Explore

+
    {mainLinks.map((link) =>
  • {link.icon}{link.label}
  • )}
+
+
+

Community Stats

+
{stats.map((stat, index) =>
{stat.icon}{stat.label}
{stat.value}
)}
+
Join Newsletter
+
+
+
+
+
+
+
+
© {new Date().getFullYear()} CFC Game DevAll rights reserved
+ + Constitution +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
v
+
+
+ +
Made within Perth, UWA
+
+
+
+
; +} From 90b119d1675d884b18321adf9279eebe0c181bc3 Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 26 Nov 2025 17:18:11 +0800 Subject: [PATCH 02/13] chore: update footer logo asset --- client/public/navbar_arr.png | Bin 0 -> 5315 bytes client/src/components/footer.tsx | 41 ++++++++++++++++++++++++------- 2 files changed, 32 insertions(+), 9 deletions(-) create mode 100644 client/public/navbar_arr.png diff --git a/client/public/navbar_arr.png b/client/public/navbar_arr.png new file mode 100644 index 0000000000000000000000000000000000000000..557f7c14d0618621dd034bf3fa933b8440a5ec4f GIT binary patch literal 5315 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 }, { label: "About", href: "/about", icon: }, { label: "Events", href: "/events", icon: }, { label: "Game Showcase", href: "/game-showcase", icon: }, { label: "Art Showcase", href: "/art-showcase", icon: }]; + const mainLinks = [{ label: "Home", href: "/", icon: }, { label: "About", href: "/about", icon: }, { label: "Events", href: "/events", icon: }, { label: "Games", href: "/games", icon: }, { label: "Artwork", href: "/artwork", icon: }]; const socialLinks = [ { icon: , href: "#", label: "Discord", color: "hover:text-[#5865F2]" }, { icon: , href: "#", label: "X (Twitter)", color: "hover:text-white" }, @@ -162,7 +162,10 @@ export function Footer() { const stats = [{ label: "Active Members", value: "150+", icon: }, { label: "Games Created", value: "50+", icon: }, { label: "Events Hosted", value: "25+", icon: }]; return
setIsHovering(false)}> -
+
{isClient && } @@ -171,12 +174,29 @@ export function Footer() {
-
- - - UWA Game Development Logo + {/* UPDATED: Logo now blends directly with background - no box container */} +
+ + {/* Arrow logo - transparent PNG, no background box */} + CFC Game Development Logo + + -

Game Development

@@ -206,7 +226,10 @@ export function Footer() {
© {new Date().getFullYear()} CFC Game DevAll rights reserved
- + Constitution
@@ -232,4 +255,4 @@ export function Footer() {
; -} +} \ No newline at end of file From a07990c527a78d9f3f2589a243c0424c5b464e53 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 30 Nov 2025 19:54:25 +0800 Subject: [PATCH 03/13] Merge main into issue-3-Create_footer_component, integrate Footer component, and fix layout for large displays --- client/package-lock.json | 43 ++++++++++++++++++++++++++++++++++++++ client/package.json | 3 ++- client/src/pages/index.tsx | 24 +++++++++++++-------- 3 files changed, 60 insertions(+), 10 deletions(-) diff --git a/client/package-lock.json b/client/package-lock.json index 84564d6..9d80d20 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -15,6 +15,7 @@ "axios": "^1.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "is-inside-container": "^1.0.0", "lucide-react": "^0.516.0", "next": "15.4.7", @@ -3667,6 +3668,33 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/framer-motion": { + "version": "12.23.24", + "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.24.tgz", + "integrity": "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==", + "license": "MIT", + "dependencies": { + "motion-dom": "^12.23.23", + "motion-utils": "^12.23.6", + "tslib": "^2.4.0" + }, + "peerDependencies": { + "@emotion/is-prop-valid": "*", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@emotion/is-prop-valid": { + "optional": true + }, + "react": { + "optional": true + }, + "react-dom": { + "optional": true + } + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4963,6 +4991,21 @@ "node": ">=16 || 14 >=14.17" } }, + "node_modules/motion-dom": { + "version": "12.23.23", + "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", + "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", + "license": "MIT", + "dependencies": { + "motion-utils": "^12.23.6" + } + }, + "node_modules/motion-utils": { + "version": "12.23.6", + "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", + "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", diff --git a/client/package.json b/client/package.json index e38c24c..afcc5f5 100644 --- a/client/package.json +++ b/client/package.json @@ -2,7 +2,7 @@ "name": "client", "version": "0.1.0", "private": true, - "engines":{ + "engines": { "node": ">=20.0.0" }, "scripts": { @@ -25,6 +25,7 @@ "axios": "^1.12.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "framer-motion": "^12.23.24", "is-inside-container": "^1.0.0", "lucide-react": "^0.516.0", "next": "15.4.7", diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 9d98316..87ac8a4 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -3,6 +3,7 @@ import { useState } from "react"; import { usePings } from "@/hooks/pings"; import { cn } from "@/lib/utils"; +import { Footer } from "@/components/footer"; import { Button } from "../components/ui/button"; export default function Home() { @@ -12,15 +13,20 @@ export default function Home() { }); return ( -
-

Test title

-

Test subtitle

- -

- Response from server: {data as string} -

+
+
+
+

Test title

+

Test subtitle

+ +

+ Response from server: {data as string} +

+
+
+
); } From 5e16c4c9a30c1c249944cff51a286e6f687bead0 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 30 Nov 2025 20:53:23 +0800 Subject: [PATCH 04/13] Merge main into issue-3-Create_footer_component and enhance Footer component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Integrate Footer component with framer-motion animations - Fix footer layout to properly display on large displays (27" monitor) - Remove Community Stats section from footer - Fix Constitution button alignment with gamepad icon divider - Add email address (UWAgamedev@gmail.com) to footer - Apply pixel font (font-jersey10) to all footer text for retro gaming aesthetic - Increase font sizes throughout footer for better readability - Reposition email address directly under 'Create • Play • Inspire' tagline - Update navigation link labels (About -> About Us, Games -> Games Showcase) - Add framer-motion dependency to package.json --- client/src/components/footer.tsx | 37 +++++++++++++++----------------- 1 file changed, 17 insertions(+), 20 deletions(-) diff --git a/client/src/components/footer.tsx b/client/src/components/footer.tsx index ffa0bb2..ff74e2d 100644 --- a/client/src/components/footer.tsx +++ b/client/src/components/footer.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import Image from "next/image"; import { useState, useRef, useEffect } from "react"; import { motion, useMotionValue, useSpring, useTransform, useMotionTemplate, MotionValue } from "framer-motion"; -import { Gamepad2, Sparkles, Code2, Palette, Calendar, Users, ChevronRight, Heart, Zap, Trophy, Star } from "lucide-react"; +import { Gamepad2, Sparkles, Code2, Palette, Calendar, Users, ChevronRight, Heart, Zap } from "lucide-react"; // Type definitions for particle system type ParticleConfig = { baseX: number; baseY: number; size: number; delay: number; duration: number; color?: string }; @@ -151,7 +151,7 @@ export function Footer() { setIsHovering(true); }; - const mainLinks = [{ label: "Home", href: "/", icon: }, { label: "About", href: "/about", icon: }, { label: "Events", href: "/events", icon: }, { label: "Games", href: "/games", icon: }, { label: "Artwork", href: "/artwork", icon: }]; + const mainLinks = [{ label: "Home", href: "/", icon: }, { label: "About Us", href: "/about", icon: }, { label: "Events", href: "/events", icon: }, { label: "Games Showcase", href: "/games", icon: }, { label: "Artwork", href: "/artwork", icon: }]; const socialLinks = [ { icon: , href: "#", label: "Discord", color: "hover:text-[#5865F2]" }, { icon: , href: "#", label: "X (Twitter)", color: "hover:text-white" }, @@ -159,7 +159,6 @@ export function Footer() { { icon: , href: "#", label: "YouTube", color: "hover:text-[#FF0000]" }, ]; const quickLinks = [{ label: "Join the Club", href: "#" }, { label: "Submit Your Game", href: "#" }, { label: "Upcoming Jams", href: "#" }, { label: "Resources", href: "#" }]; - const stats = [{ label: "Active Members", value: "150+", icon: }, { label: "Games Created", value: "50+", icon: }, { label: "Events Hosted", value: "25+", icon: }]; return
setIsHovering(false)}>
{particleConfigs.map((cfg, i) => )}
}
-
+
{/* UPDATED: Logo now blends directly with background - no box container */}
@@ -199,25 +198,23 @@ export function Footer() {
-

Game Development

-

Create • Play • Inspire

+

Game Development

+

Create • Play • Inspire

-

Building the next generation of game developers at UWA game development club

+ + UWAgamedev@gmail.com + +

Building the next generation of game developers at UWA game development club

{socialLinks.map((social, index) => {social.icon})}
-

Quick Links

-
    {quickLinks.map((link) =>
  • setIsHovered(link.label)} onMouseLeave={() => setIsHovered(null)}>{link.label}{isHovered === link.label && }
  • )}
+

Quick Links

+
    {quickLinks.map((link) =>
  • setIsHovered(link.label)} onMouseLeave={() => setIsHovered(null)}>{link.label}{isHovered === link.label && }
  • )}
-

Explore

-
    {mainLinks.map((link) =>
  • {link.icon}{link.label}
  • )}
-
-
-

Community Stats

-
{stats.map((stat, index) =>
{stat.icon}{stat.label}
{stat.value}
)}
-
Join Newsletter
+

Explore

+
    {mainLinks.map((link) =>
  • {link.icon}{link.label}
  • )}
@@ -225,12 +222,12 @@ export function Footer() {
-
© {new Date().getFullYear()} CFC Game DevAll rights reserved
+
© {new Date().getFullYear()} CFC Game DevAll rights reserved
- Constitution + Constitution
@@ -250,7 +247,7 @@ export function Footer() {
-
Made within Perth, UWA
+
Made within Perth, UWA
From 4dff562d10157ee08645767db6d55bfe0c2571bf Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 3 Dec 2025 11:14:59 +0800 Subject: [PATCH 05/13] Fix Prettier formatting --- client/src/components/footer.tsx | 576 ++++++++++++++++++++++++------- client/src/pages/index.tsx | 4 +- 2 files changed, 458 insertions(+), 122 deletions(-) diff --git a/client/src/components/footer.tsx b/client/src/components/footer.tsx index ff74e2d..3409f04 100644 --- a/client/src/components/footer.tsx +++ b/client/src/components/footer.tsx @@ -2,41 +2,137 @@ import Link from "next/link"; import Image from "next/image"; import { useState, useRef, useEffect } from "react"; -import { motion, useMotionValue, useSpring, useTransform, useMotionTemplate, MotionValue } from "framer-motion"; -import { Gamepad2, Sparkles, Code2, Palette, Calendar, Users, ChevronRight, Heart, Zap } from "lucide-react"; +import { + motion, + useMotionValue, + useSpring, + useTransform, + useMotionTemplate, + MotionValue, +} from "framer-motion"; +import { + Gamepad2, + Sparkles, + Code2, + Palette, + Calendar, + Users, + ChevronRight, + Heart, + Zap, +} from "lucide-react"; // Type definitions for particle system -type ParticleConfig = { baseX: number; baseY: number; size: number; delay: number; duration: number; color?: string }; -type NetworkParticle = { x: number; y: number; vx: number; vy: number; size: number }; +type ParticleConfig = { + baseX: number; + baseY: number; + size: number; + delay: number; + duration: number; + color?: string; +}; +type NetworkParticle = { + x: number; + y: number; + vx: number; + vy: number; + size: number; +}; // Gradient that follows mouse cursor position for interactive effect -function MouseGradient({ smoothX, smoothY, isHovering }: { smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean }) { +function MouseGradient({ + smoothX, + smoothY, + isHovering, +}: { + smoothX: MotionValue; + smoothY: MotionValue; + isHovering: boolean; +}) { const background = useMotionTemplate`radial-gradient(circle 600px at ${smoothX}% ${smoothY}%, rgba(139, 92, 246, 0.3) 0%, rgba(236, 72, 153, 0.12) 40%, transparent 70%)`; - return ; + return ( + + ); } // Individual particle component with pulsing animation and mouse interaction -function SimpleParticle({ baseX, baseY, size, delay, duration, smoothX, smoothY, isHovering, color = "rgba(255, 255, 255, 0.6)" }: ParticleConfig & { smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean }) { +function SimpleParticle({ + baseX, + baseY, + size, + delay, + duration, + smoothX, + smoothY, + isHovering, + color = "rgba(255, 255, 255, 0.6)", +}: ParticleConfig & { + smoothX: MotionValue; + smoothY: MotionValue; + isHovering: boolean; +}) { // Particles react to mouse movement when hovering - const offsetX = useTransform(smoothX, (mx) => isHovering ? (mx - 50) * 0.3 : 0); - const offsetY = useTransform(smoothY, (my) => isHovering ? (my - 50) * 0.3 : 0); - return - -
+ const offsetX = useTransform(smoothX, (mx) => + isHovering ? (mx - 50) * 0.3 : 0, + ); + const offsetY = useTransform(smoothY, (my) => + isHovering ? (my - 50) * 0.3 : 0, + ); + return ( + + +
+ - ; + ); } // Canvas-based network visualization with particle connections -function NetworkCanvas({ width, height, smoothX, smoothY, isHovering }: { width: number; height: number; smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean }) { +function NetworkCanvas({ + width, + height, + smoothX, + smoothY, + isHovering, +}: { + width: number; + height: number; + smoothX: MotionValue; + smoothY: MotionValue; + isHovering: boolean; +}) { const canvasRef = useRef(null); const particlesRef = useRef([]); const initializedRef = useRef(false); - + useEffect(() => { // Initialize particles only once to prevent regeneration on re-render if (!initializedRef.current) { - particlesRef.current = Array.from({ length: 22 }, () => ({ x: Math.random() * width, y: Math.random() * height, vx: (Math.random() - 0.5) * 0.15, vy: (Math.random() - 0.5) * 0.15, size: 1.5 + Math.random() * 1.5 })); + particlesRef.current = Array.from({ length: 22 }, () => ({ + x: Math.random() * width, + y: Math.random() * height, + vx: (Math.random() - 0.5) * 0.15, + vy: (Math.random() - 0.5) * 0.15, + size: 1.5 + Math.random() * 1.5, + })); initializedRef.current = true; } const canvas = canvasRef.current; @@ -44,13 +140,13 @@ function NetworkCanvas({ width, height, smoothX, smoothY, isHovering }: { width: const ctx = canvas.getContext("2d"); if (!ctx) return; let id: number; - + const render = () => { ctx.clearRect(0, 0, width, height); const pts = particlesRef.current; const mx = (smoothX.get() / 100) * width; // Convert percentage to pixel coordinates const my = (smoothY.get() / 100) * height; - + // Update particle positions with boundary collision pts.forEach((p) => { p.x += p.vx; @@ -60,16 +156,21 @@ function NetworkCanvas({ width, height, smoothX, smoothY, isHovering }: { width: p.x = Math.max(0, Math.min(width, p.x)); p.y = Math.max(0, Math.min(height, p.y)); }); - + // Draw connections between nearby particles (max 2 connections per particle) for (let i = 0; i < pts.length; i++) { const pA = pts[i]; - const dists = pts.slice(i + 1).map((pB, j) => { - const dx = pA.x - pB.x; - const dy = pA.y - pB.y; - return { index: i + j + 1, dist: Math.sqrt(dx * dx + dy * dy) }; - }).filter(d => d.dist < 150).sort((a, b) => a.dist - b.dist).slice(0, 2); - + const dists = pts + .slice(i + 1) + .map((pB, j) => { + const dx = pA.x - pB.x; + const dy = pA.y - pB.y; + return { index: i + j + 1, dist: Math.sqrt(dx * dx + dy * dy) }; + }) + .filter((d) => d.dist < 150) + .sort((a, b) => a.dist - b.dist) + .slice(0, 2); + dists.forEach(({ index, dist }) => { const pB = pts[index]; const op = (1 - dist / 150) * 0.25; // Opacity decreases with distance @@ -85,7 +186,7 @@ function NetworkCanvas({ width, height, smoothX, smoothY, isHovering }: { width: ctx.stroke(); }); } - + // Draw connections from particles to mouse cursor when hovering if (isHovering) { pts.forEach((p) => { @@ -111,7 +212,15 @@ function NetworkCanvas({ width, height, smoothX, smoothY, isHovering }: { width: render(); return () => cancelAnimationFrame(id); }, [width, height, smoothX, smoothY, isHovering]); - return ; + return ( + + ); } export function Footer() { @@ -121,7 +230,7 @@ export function Footer() { const [dimensions, setDimensions] = useState({ width: 1920, height: 400 }); const [isClient, setIsClient] = useState(false); // Prevent SSR issues with canvas/animations const [particleConfigs, setParticleConfigs] = useState([]); - + // Mouse position tracking with spring physics for smooth movement const mouseX = useMotionValue(50); const mouseY = useMotionValue(50); @@ -131,16 +240,35 @@ export function Footer() { // Initialize particles on client-side only (prevents hydration mismatch) useEffect(() => { setIsClient(true); - const colors = ["rgba(255, 255, 255, 0.5)", "rgba(196, 181, 253, 0.4)", "rgba(168, 85, 247, 0.4)", "rgba(236, 72, 153, 0.4)"]; - setParticleConfigs(Array.from({ length: 22 }, () => ({ baseX: Math.random() * 100, baseY: Math.random() * 100, size: 2 + Math.random() * 3, delay: Math.random() * 4, duration: 3 + Math.random() * 3, color: colors[Math.floor(Math.random() * colors.length)] }))); + const colors = [ + "rgba(255, 255, 255, 0.5)", + "rgba(196, 181, 253, 0.4)", + "rgba(168, 85, 247, 0.4)", + "rgba(236, 72, 153, 0.4)", + ]; + setParticleConfigs( + Array.from({ length: 22 }, () => ({ + baseX: Math.random() * 100, + baseY: Math.random() * 100, + size: 2 + Math.random() * 3, + delay: Math.random() * 4, + duration: 3 + Math.random() * 3, + color: colors[Math.floor(Math.random() * colors.length)], + })), + ); }, []); // Update canvas dimensions on window resize useEffect(() => { - const update = () => footerRef.current && setDimensions({ width: footerRef.current.offsetWidth, height: footerRef.current.offsetHeight }); + const update = () => + footerRef.current && + setDimensions({ + width: footerRef.current.offsetWidth, + height: footerRef.current.offsetHeight, + }); update(); - window.addEventListener('resize', update); - return () => window.removeEventListener('resize', update); + window.addEventListener("resize", update); + return () => window.removeEventListener("resize", update); }, []); // Convert mouse coordinates to percentage for gradient positioning @@ -151,105 +279,311 @@ export function Footer() { setIsHovering(true); }; - const mainLinks = [{ label: "Home", href: "/", icon: }, { label: "About Us", href: "/about", icon: }, { label: "Events", href: "/events", icon: }, { label: "Games Showcase", href: "/games", icon: }, { label: "Artwork", href: "/artwork", icon: }]; + const mainLinks = [ + { label: "Home", href: "/", icon: }, + { label: "About Us", href: "/about", icon: }, + { + label: "Events", + href: "/events", + icon: , + }, + { + label: "Games Showcase", + href: "/games", + icon: , + }, + { + label: "Artwork", + href: "/artwork", + icon: , + }, + ]; const socialLinks = [ - { icon: , href: "#", label: "Discord", color: "hover:text-[#5865F2]" }, - { icon: , href: "#", label: "X (Twitter)", color: "hover:text-white" }, - { icon: , href: "#", label: "GitHub", color: "hover:text-white" }, - { icon: , href: "#", label: "YouTube", color: "hover:text-[#FF0000]" }, + { + icon: ( + + + + ), + href: "#", + label: "Discord", + color: "hover:text-[#5865F2]", + }, + { + icon: ( + + + + ), + href: "#", + label: "X (Twitter)", + color: "hover:text-white", + }, + { + icon: ( + + + + ), + href: "#", + label: "GitHub", + color: "hover:text-white", + }, + { + icon: ( + + + + ), + href: "#", + label: "YouTube", + color: "hover:text-[#FF0000]", + }, + ]; + const quickLinks = [ + { label: "Join the Club", href: "#" }, + { label: "Submit Your Game", href: "#" }, + { label: "Upcoming Jams", href: "#" }, + { label: "Resources", href: "#" }, ]; - const quickLinks = [{ label: "Join the Club", href: "#" }, { label: "Submit Your Game", href: "#" }, { label: "Upcoming Jams", href: "#" }, { label: "Resources", href: "#" }]; - return
setIsHovering(false)}> -
- -
- {isClient && } - {isClient &&
{particleConfigs.map((cfg, i) => )}
} -
-
-
-
- {/* UPDATED: Logo now blends directly with background - no box container */} -
- - {/* Arrow logo - transparent PNG, no background box */} - CFC Game Development Logo - setIsHovering(false)} + > +
+ +
+ {isClient && ( + + )} + {isClient && ( +
+ {particleConfigs.map((cfg, i) => ( + + ))} +
+ )} +
+
+
+
+ {/* UPDATED: Logo now blends directly with background - no box container */} +
+ - + {/* Arrow logo - transparent PNG, no background box */} + CFC Game Development Logo + + + - -
-

Game Development

-

Create • Play • Inspire

+
+

+ Game Development +

+

+ Create • Play • Inspire +

+
+
+ + UWAgamedev@gmail.com + +

+ Building the next generation of game developers at UWA game + development club +

+
+ {socialLinks.map((social, index) => ( + + + {social.icon} + + + ))}
- - UWAgamedev@gmail.com - -

Building the next generation of game developers at UWA game development club

-
{socialLinks.map((social, index) => {social.icon})}
-
-
-

Quick Links

-
    {quickLinks.map((link) =>
  • setIsHovered(link.label)} onMouseLeave={() => setIsHovered(null)}>{link.label}{isHovered === link.label && }
  • )}
+
+

+ + Quick Links +

+
    + {quickLinks.map((link) => ( +
  • + setIsHovered(link.label)} + onMouseLeave={() => setIsHovered(null)} + > + + + {link.label} + {isHovered === link.label && ( + + )} + + +
  • + ))} +
+
+
+

+ + Explore +

+
    + {mainLinks.map((link) => ( +
  • + + + {link.icon} + + {link.label} + +
  • + ))} +
+
-
-

Explore

-
    {mainLinks.map((link) =>
  • {link.icon}{link.label}
  • )}
+
+
+
+
+
+ + + +
-
-
-
-
-
-
-
© {new Date().getFullYear()} CFC Game DevAll rights reserved
- - Constitution -
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+ © {new Date().getFullYear()} CFC Game Dev + + All rights reserved +
+ + + Constitution + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ v
+
-
v
-
+ +
+ Made with + + + + in Perth, UWA
- -
Made within Perth, UWA
+
-
-
; -} \ No newline at end of file +
+ ); +} diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 87ac8a4..400c8b6 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -17,7 +17,9 @@ export default function Home() {

Test title

-

Test subtitle

+

+ Test subtitle +

From c9de6a0dc8453d1b293f6f83561eb4c08ec98c2c Mon Sep 17 00:00:00 2001 From: Oliver Date: Wed, 3 Dec 2025 11:24:25 +0800 Subject: [PATCH 06/13] Add root package-lock.json --- package-lock.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 package-lock.json diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..1aa4fe2 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6 @@ +{ + "name": "game-dev", + "lockfileVersion": 3, + "requires": true, + "packages": {} +} From d6ee4afeb6caba418ab21eae908360137e828095 Mon Sep 17 00:00:00 2001 From: samjjacko Date: Thu, 4 Dec 2025 08:36:57 +0800 Subject: [PATCH 07/13] Improved colour usage, removed the tree of divs, moved Footer into correct directory and attached it to _app.tsx --- client/README.md | 1 + .../{footer.tsx => main/Footer.tsx} | 175 +++++++----------- client/src/pages/_app.tsx | 2 + client/src/pages/index.tsx | 2 - client/src/styles/globals.css | 1 + 5 files changed, 71 insertions(+), 110 deletions(-) rename client/src/components/{footer.tsx => main/Footer.tsx} (71%) diff --git a/client/README.md b/client/README.md index c02209c..bacff64 100644 --- a/client/README.md +++ b/client/README.md @@ -7,6 +7,7 @@ This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next - Axios for data fetching - shadcn/ui for components - lucide for icons +- react-social-icons for social media/brand icons ## Getting Started diff --git a/client/src/components/footer.tsx b/client/src/components/main/Footer.tsx similarity index 71% rename from client/src/components/footer.tsx rename to client/src/components/main/Footer.tsx index 3409f04..2099761 100644 --- a/client/src/components/footer.tsx +++ b/client/src/components/main/Footer.tsx @@ -1,26 +1,27 @@ "use client"; -import Link from "next/link"; -import Image from "next/image"; -import { useState, useRef, useEffect } from "react"; import { motion, + MotionValue, + useMotionTemplate, useMotionValue, useSpring, useTransform, - useMotionTemplate, - MotionValue, } from "framer-motion"; import { - Gamepad2, - Sparkles, - Code2, - Palette, Calendar, - Users, ChevronRight, + Code2, + Gamepad2, Heart, + Palette, + Sparkles, + Users, Zap, } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect,useRef, useState } from "react"; +import { SocialIcon } from "react-social-icons"; // Type definitions for particle system type ParticleConfig = { @@ -59,6 +60,13 @@ function MouseGradient({ ); } +// should probably belong in src/utils +function cssVarAsHSL(cssvar: string, alpha?: number) { + // client side only + const col = window.getComputedStyle(document.body).getPropertyValue(cssvar); + return alpha !== undefined ? `hsl(${col} / ${alpha})` : `hsl(${col})`; +} + // Individual particle component with pulsing animation and mouse interaction function SimpleParticle({ baseX, @@ -69,7 +77,7 @@ function SimpleParticle({ smoothX, smoothY, isHovering, - color = "rgba(255, 255, 255, 0.6)", + color = cssVarAsHSL("--light-1", 0.6), }: ParticleConfig & { smoothX: MotionValue; smoothY: MotionValue; @@ -88,7 +96,8 @@ function SimpleParticle({ style={{ left: `${baseX}%`, top: `${baseY}%`, x: offsetX, y: offsetY }} >
@@ -172,12 +181,14 @@ function NetworkCanvas({ .slice(0, 2); dists.forEach(({ index, dist }) => { + // a little bit redundant as the lines aren't super prominent, can probably + // just use white here const pB = pts[index]; const op = (1 - dist / 150) * 0.25; // Opacity decreases with distance const grad = ctx.createLinearGradient(pA.x, pA.y, pB.x, pB.y); - grad.addColorStop(0, `rgba(139, 92, 246, ${op})`); - grad.addColorStop(0.5, `rgba(196, 181, 253, ${op * 1.5})`); - grad.addColorStop(1, `rgba(236, 72, 153, ${op})`); + grad.addColorStop(0, cssVarAsHSL("--logo-blue-1", op)); + grad.addColorStop(0.5, cssVarAsHSL("--light-2", op * 1.5)); + grad.addColorStop(1, cssVarAsHSL("--light-alt", op)); ctx.strokeStyle = grad; ctx.lineWidth = 1.5; ctx.beginPath(); @@ -196,8 +207,8 @@ function NetworkCanvas({ if (dist < 120) { const op = (1 - dist / 120) * 0.4; const grad = ctx.createLinearGradient(p.x, p.y, mx, my); - grad.addColorStop(0, `rgba(236, 72, 153, ${op})`); - grad.addColorStop(1, `rgba(255, 255, 255, ${op * 0.5})`); + grad.addColorStop(0, cssVarAsHSL("--light-alt", op)); + grad.addColorStop(1, cssVarAsHSL("--light-1", op * 0.5)); ctx.strokeStyle = grad; ctx.lineWidth = 2; ctx.beginPath(); @@ -223,7 +234,7 @@ function NetworkCanvas({ ); } -export function Footer() { +export default function Footer() { const footerRef = useRef(null); const [isHovered, setIsHovered] = useState(null); // Track which link is hovered for underline effect const [isHovering, setIsHovering] = useState(false); @@ -236,15 +247,15 @@ export function Footer() { const mouseY = useMotionValue(50); const smoothX = useSpring(mouseX, { damping: 50, stiffness: 100 }); const smoothY = useSpring(mouseY, { damping: 50, stiffness: 100 }); - + const motionColoursRef = useRef>({}); // Initialize particles on client-side only (prevents hydration mismatch) useEffect(() => { setIsClient(true); - const colors = [ - "rgba(255, 255, 255, 0.5)", - "rgba(196, 181, 253, 0.4)", - "rgba(168, 85, 247, 0.4)", - "rgba(236, 72, 153, 0.4)", + const particlecolors = [ + cssVarAsHSL("--light-1", 0.5), + cssVarAsHSL("--light-2", 0.4), + cssVarAsHSL("--light-alt", 0.4), + // cssVarAsHSL("--logo-blue-2", 0.4), ]; setParticleConfigs( Array.from({ length: 22 }, () => ({ @@ -253,9 +264,21 @@ export function Footer() { size: 2 + Math.random() * 3, delay: Math.random() * 4, duration: 3 + Math.random() * 3, - color: colors[Math.floor(Math.random() * colors.length)], + color: + particlecolors[Math.floor(Math.random() * particlecolors.length)], })), ); + // unfortunatly, motion cannot animate named colours. + // one readable solution is to construct a handful of constants on the client instead. + motionColoursRef.current = { + mouseGradStart: cssVarAsHSL("--light-alt", 0.3), + mouseGradEnd: cssVarAsHSL("--light-2", 0.3), + // radial-gradient(at 20% 30%, hsl(--light-2 / 0.3) 0px, transparent 50%), + // radial-gradient(at 80% 70%, hsl(--light-alt / 0.3) 0px, transparent 50%), + // radial-gradient(at 50% 50%, hsl(--logo-blue-1 / 0.2) 0px, transparent 50%)`, + socialBGHov: cssVarAsHSL("--light-1", 0.1), + socialBorderHov: cssVarAsHSL("--light-alt", 0.5), + }; }, []); // Update canvas dimensions on window resize @@ -278,7 +301,8 @@ export function Footer() { mouseY.set(((e.clientY - rect.top) / rect.height) * 100); setIsHovering(true); }; - + // ideally this should pull from some source of information with the nav, but + // it's not that big of a deal const mainLinks = [ { label: "Home", href: "/", icon: }, { label: "About Us", href: "/about", icon: }, @@ -298,47 +322,15 @@ export function Footer() { icon: , }, ]; + + // these should be stored elsewhere... like in a data directory so + // they can be easily changed const socialLinks = [ - { - icon: ( - - - - ), - href: "#", - label: "Discord", - color: "hover:text-[#5865F2]", - }, - { - icon: ( - - - - ), - href: "#", - label: "X (Twitter)", - color: "hover:text-white", - }, - { - icon: ( - - - - ), - href: "#", - label: "GitHub", - color: "hover:text-white", - }, - { - icon: ( - - - - ), - href: "#", - label: "YouTube", - color: "hover:text-[#FF0000]", - }, + // we can easily infer the domain + { url: "https://discord.com", label: "Discord" }, + { url: "https://twitter.com", label: "X (Twitter)" }, + { url: "https://github.com", label: "GitHub" }, + { url: "https://youtube.com", label: "YouTube" }, ]; const quickLinks = [ { label: "Join the Club", href: "#" }, @@ -346,7 +338,6 @@ export function Footer() { { label: "Upcoming Jams", href: "#" }, { label: "Resources", href: "#" }, ]; - return (
setIsHovering(false)} > -
+
-
{isClient && (
{socialLinks.map((social, index) => ( - - {social.icon} + - + ))}

- + Quick Links

    @@ -548,28 +529,6 @@ export function Footer() { Constitution -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    -
    - v -
    -
    -
    Made with diff --git a/client/src/pages/_app.tsx b/client/src/pages/_app.tsx index eb47676..ca3770d 100644 --- a/client/src/pages/_app.tsx +++ b/client/src/pages/_app.tsx @@ -5,6 +5,7 @@ import { ReactQueryDevtools } from "@tanstack/react-query-devtools"; import type { AppProps } from "next/app"; import { Fira_Code, Inter as FontSans, Jersey_10 } from "next/font/google"; +import Footer from "@/components/main/Footer"; import Navbar from "@/components/main/Navbar"; const fontSans = FontSans({ @@ -36,6 +37,7 @@ export default function App({ Component, pageProps }: AppProps) { > +
); diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 400c8b6..419850c 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -3,7 +3,6 @@ import { useState } from "react"; import { usePings } from "@/hooks/pings"; import { cn } from "@/lib/utils"; -import { Footer } from "@/components/footer"; import { Button } from "../components/ui/button"; export default function Home() { @@ -28,7 +27,6 @@ export default function Home() {

-
); } diff --git a/client/src/styles/globals.css b/client/src/styles/globals.css index 5f6dff0..c7be7e5 100644 --- a/client/src/styles/globals.css +++ b/client/src/styles/globals.css @@ -52,6 +52,7 @@ --input: 235 47% 20%; --ring: 236 47% 7%; --radius: 0.5rem; + } } From db31a496de5d5634529af67a79e445f947a16311 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sat, 6 Dec 2025 14:12:37 +0800 Subject: [PATCH 08/13] refactor(footer): address code review feedback and improve code organisation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Extract all magic numbers to footer-constants.ts for maintainability - Move link data to footer-data.tsx for easy updates - Create SocialIconButton component for reusable social media icons - Create FooterLinkList component for consistent link list rendering - Move cssVarAsHSL utility function to lib/utils.ts - Refactor Footer.tsx to use new components, constants, and data files - Fix British spelling: particlecolors → particlecolours - Replace hard-coded rgba values with Motion values using CSS variables - Remove animate-pulse from Gamepad2 icon (no functional purpose) - Simplify nested div structure where possible - All colors now use Motion values or CSS variables (no hard-coded rgba) Addresses all code review feedback from Sam: - Named constants instead of magic numbers - British spelling for colours - Component extraction for reusability - Data organisation for easy updates - Motion values for colours - Simplified structure --- .../src/components/footer/FooterLinkList.tsx | 78 ++++ .../components/footer/SocialIconButton.tsx | 80 +++++ client/src/components/main/Footer.tsx | 336 +++++++++--------- client/src/lib/footer-constants.ts | 65 ++++ client/src/lib/utils.ts | 16 + 5 files changed, 399 insertions(+), 176 deletions(-) create mode 100644 client/src/components/footer/FooterLinkList.tsx create mode 100644 client/src/components/footer/SocialIconButton.tsx create mode 100644 client/src/lib/footer-constants.ts diff --git a/client/src/components/footer/FooterLinkList.tsx b/client/src/components/footer/FooterLinkList.tsx new file mode 100644 index 0000000..a624d3c --- /dev/null +++ b/client/src/components/footer/FooterLinkList.tsx @@ -0,0 +1,78 @@ +"use client"; + +import { ChevronRight } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; +import type { ReactNode } from "react"; + +interface FooterLink { + label: string; + href: string; + icon?: ReactNode; +} + +interface FooterLinkListProps { + title: string; + titleIcon: ReactNode; + links: FooterLink[]; + useChevron?: boolean; // If true, uses ChevronRight; if false, uses icon from link +} + +/** + * Reusable footer link list component + * Supports both icon-based links (mainLinks) and chevron-based links (quickLinks) + * Provides consistent hover states and styling + */ +export default function FooterLinkList({ + title, + titleIcon, + links, + useChevron = false, +}: FooterLinkListProps) { + const [isHovered, setIsHovered] = useState(null); + + return ( +
+

+ {titleIcon} + {title} +

+
    + {links.map((link) => ( +
  • + setIsHovered(link.label)} + onMouseLeave={() => setIsHovered(null)} + > + {useChevron ? ( + <> + + + {link.label} + {isHovered === link.label && ( + + )} + + + ) : ( + <> + + {link.icon} + + {link.label} + + )} + +
  • + ))} +
+
+ ); +} + diff --git a/client/src/components/footer/SocialIconButton.tsx b/client/src/components/footer/SocialIconButton.tsx new file mode 100644 index 0000000..b4301cf --- /dev/null +++ b/client/src/components/footer/SocialIconButton.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { motion } from "framer-motion"; +import { SocialIcon } from "react-social-icons"; + +import { + MOTION_COLOUR_SOCIAL_BG_HOV_ALPHA, + MOTION_COLOUR_SOCIAL_BORDER_HOV_ALPHA, + SOCIAL_ICON_HOVER_SCALE, + SOCIAL_ICON_HOVER_Y, + SOCIAL_ICON_ROTATE_DEGREES, + SOCIAL_ICON_SPRING_DAMPING, + SOCIAL_ICON_SPRING_STIFFNESS, + SOCIAL_ICON_TAP_SCALE, +} from "@/lib/footer-constants"; +import { cssVarAsHSL } from "@/lib/utils"; + +interface SocialIconButtonProps { + url: string; + label: string; + motionColours: { + socialBGHov: string; + socialBorderHov: string; + }; +} + +/** + * Reusable social media icon button component + * Handles hover animations and styling with Motion values for colors + */ +export default function SocialIconButton({ + url, + label, + motionColours, +}: SocialIconButtonProps) { + return ( + + + + + + ); +} + +/** + * Helper function to create motion colours for social icons + * This ensures colors use Motion values instead of hard-coded rgba + */ +export function createSocialMotionColours() { + return { + socialBGHov: cssVarAsHSL("--light-1", MOTION_COLOUR_SOCIAL_BG_HOV_ALPHA), + socialBorderHov: cssVarAsHSL( + "--light-alt", + MOTION_COLOUR_SOCIAL_BORDER_HOV_ALPHA, + ), + }; +} + diff --git a/client/src/components/main/Footer.tsx b/client/src/components/main/Footer.tsx index 2099761..30299a1 100644 --- a/client/src/components/main/Footer.tsx +++ b/client/src/components/main/Footer.tsx @@ -7,21 +7,53 @@ import { useSpring, useTransform, } from "framer-motion"; -import { - Calendar, - ChevronRight, - Code2, - Gamepad2, - Heart, - Palette, - Sparkles, - Users, - Zap, -} from "lucide-react"; +import { Code2, Gamepad2, Heart, Sparkles, Zap } from "lucide-react"; import Image from "next/image"; import Link from "next/link"; -import { useEffect,useRef, useState } from "react"; -import { SocialIcon } from "react-social-icons"; +import { useEffect, useRef, useState } from "react"; + +import FooterLinkList from "@/components/footer/FooterLinkList"; +import SocialIconButton, { + createSocialMotionColours, +} from "@/components/footer/SocialIconButton"; +import { mainLinks, quickLinks, socialLinks } from "@/data/footer-data"; +import { + DEFAULT_FOOTER_HEIGHT, + DEFAULT_FOOTER_WIDTH, + GRADIENT_CIRCLE_SIZE, + GRADIENT_OPACITY_DEFAULT, + GRADIENT_OPACITY_HOVERING, + GRADIENT_TRANSITION_DURATION, + MOUSE_CENTER_OFFSET, + MOUSE_OFFSET_MULTIPLIER, + MOTION_COLOUR_MOUSE_GRAD_END_ALPHA, + MOTION_COLOUR_MOUSE_GRAD_START_ALPHA, + NETWORK_CONNECTION_DISTANCE, + NETWORK_CONNECTION_MAX_PER_PARTICLE, + NETWORK_CONNECTION_OPACITY_BASE, + NETWORK_CONNECTION_OPACITY_MULTIPLIER, + NETWORK_LINE_WIDTH, + NETWORK_MOUSE_CONNECTION_DISTANCE, + NETWORK_MOUSE_CONNECTION_OPACITY_BASE, + NETWORK_MOUSE_CONNECTION_OPACITY_MULTIPLIER, + NETWORK_MOUSE_LINE_WIDTH, + NETWORK_PARTICLE_SIZE_MAX, + NETWORK_PARTICLE_SIZE_MIN, + NETWORK_PARTICLE_VELOCITY_MULTIPLIER, + PARTICLE_COLOUR_ALPHA_LIGHT_1, + PARTICLE_COLOUR_ALPHA_LIGHT_2, + PARTICLE_COLOUR_ALPHA_LIGHT_ALT, + PARTICLE_COUNT, + PARTICLE_DEFAULT_ALPHA, + PARTICLE_DELAY_MAX, + PARTICLE_DURATION_MAX, + PARTICLE_DURATION_MIN, + PARTICLE_SIZE_MAX, + PARTICLE_SIZE_MIN, + SPRING_DAMPING, + SPRING_STIFFNESS, +} from "@/lib/footer-constants"; +import { cssVarAsHSL } from "@/lib/utils"; // Type definitions for particle system type ParticleConfig = { @@ -45,28 +77,32 @@ function MouseGradient({ smoothX, smoothY, isHovering, + motionColours, }: { smoothX: MotionValue; smoothY: MotionValue; isHovering: boolean; + motionColours: { + mouseGradStart: string; + mouseGradEnd: string; + }; }) { - const background = useMotionTemplate`radial-gradient(circle 600px at ${smoothX}% ${smoothY}%, rgba(139, 92, 246, 0.3) 0%, rgba(236, 72, 153, 0.12) 40%, transparent 70%)`; + // Use Motion values for colors instead of hard-coded rgba + const background = useMotionTemplate`radial-gradient(circle ${GRADIENT_CIRCLE_SIZE}px at ${smoothX}% ${smoothY}%, ${motionColours.mouseGradStart} 0%, ${motionColours.mouseGradEnd} 40%, transparent 70%)`; return ( ); } -// should probably belong in src/utils -function cssVarAsHSL(cssvar: string, alpha?: number) { - // client side only - const col = window.getComputedStyle(document.body).getPropertyValue(cssvar); - return alpha !== undefined ? `hsl(${col} / ${alpha})` : `hsl(${col})`; -} - // Individual particle component with pulsing animation and mouse interaction function SimpleParticle({ baseX, @@ -77,7 +113,7 @@ function SimpleParticle({ smoothX, smoothY, isHovering, - color = cssVarAsHSL("--light-1", 0.6), + color = cssVarAsHSL("--light-1", PARTICLE_DEFAULT_ALPHA), }: ParticleConfig & { smoothX: MotionValue; smoothY: MotionValue; @@ -85,10 +121,14 @@ function SimpleParticle({ }) { // Particles react to mouse movement when hovering const offsetX = useTransform(smoothX, (mx) => - isHovering ? (mx - 50) * 0.3 : 0, + isHovering + ? (mx - MOUSE_CENTER_OFFSET) * MOUSE_OFFSET_MULTIPLIER + : 0, ); const offsetY = useTransform(smoothY, (my) => - isHovering ? (my - 50) * 0.3 : 0, + isHovering + ? (my - MOUSE_CENTER_OFFSET) * MOUSE_OFFSET_MULTIPLIER + : 0, ); return ( @@ -135,12 +174,16 @@ function NetworkCanvas({ useEffect(() => { // Initialize particles only once to prevent regeneration on re-render if (!initializedRef.current) { - particlesRef.current = Array.from({ length: 22 }, () => ({ + particlesRef.current = Array.from({ length: PARTICLE_COUNT }, () => ({ x: Math.random() * width, y: Math.random() * height, - vx: (Math.random() - 0.5) * 0.15, - vy: (Math.random() - 0.5) * 0.15, - size: 1.5 + Math.random() * 1.5, + vx: + (Math.random() - 0.5) * NETWORK_PARTICLE_VELOCITY_MULTIPLIER, + vy: + (Math.random() - 0.5) * NETWORK_PARTICLE_VELOCITY_MULTIPLIER, + size: + NETWORK_PARTICLE_SIZE_MIN + + Math.random() * NETWORK_PARTICLE_SIZE_MAX, })); initializedRef.current = true; } @@ -176,21 +219,24 @@ function NetworkCanvas({ const dy = pA.y - pB.y; return { index: i + j + 1, dist: Math.sqrt(dx * dx + dy * dy) }; }) - .filter((d) => d.dist < 150) + .filter((d) => d.dist < NETWORK_CONNECTION_DISTANCE) .sort((a, b) => a.dist - b.dist) - .slice(0, 2); + .slice(0, NETWORK_CONNECTION_MAX_PER_PARTICLE); dists.forEach(({ index, dist }) => { - // a little bit redundant as the lines aren't super prominent, can probably - // just use white here const pB = pts[index]; - const op = (1 - dist / 150) * 0.25; // Opacity decreases with distance + const op = + (1 - dist / NETWORK_CONNECTION_DISTANCE) * + NETWORK_CONNECTION_OPACITY_BASE; const grad = ctx.createLinearGradient(pA.x, pA.y, pB.x, pB.y); grad.addColorStop(0, cssVarAsHSL("--logo-blue-1", op)); - grad.addColorStop(0.5, cssVarAsHSL("--light-2", op * 1.5)); + grad.addColorStop( + 0.5, + cssVarAsHSL("--light-2", op * NETWORK_CONNECTION_OPACITY_MULTIPLIER), + ); grad.addColorStop(1, cssVarAsHSL("--light-alt", op)); ctx.strokeStyle = grad; - ctx.lineWidth = 1.5; + ctx.lineWidth = NETWORK_LINE_WIDTH; ctx.beginPath(); ctx.moveTo(pA.x, pA.y); ctx.lineTo(pB.x, pB.y); @@ -204,13 +250,21 @@ function NetworkCanvas({ const dx = p.x - mx; const dy = p.y - my; const dist = Math.sqrt(dx * dx + dy * dy); - if (dist < 120) { - const op = (1 - dist / 120) * 0.4; + if (dist < NETWORK_MOUSE_CONNECTION_DISTANCE) { + const op = + (1 - dist / NETWORK_MOUSE_CONNECTION_DISTANCE) * + NETWORK_MOUSE_CONNECTION_OPACITY_BASE; const grad = ctx.createLinearGradient(p.x, p.y, mx, my); grad.addColorStop(0, cssVarAsHSL("--light-alt", op)); - grad.addColorStop(1, cssVarAsHSL("--light-1", op * 0.5)); + grad.addColorStop( + 1, + cssVarAsHSL( + "--light-1", + op * NETWORK_MOUSE_CONNECTION_OPACITY_MULTIPLIER, + ), + ); ctx.strokeStyle = grad; - ctx.lineWidth = 2; + ctx.lineWidth = NETWORK_MOUSE_LINE_WIDTH; ctx.beginPath(); ctx.moveTo(p.x, p.y); ctx.lineTo(mx, my); @@ -236,48 +290,64 @@ function NetworkCanvas({ export default function Footer() { const footerRef = useRef(null); - const [isHovered, setIsHovered] = useState(null); // Track which link is hovered for underline effect const [isHovering, setIsHovering] = useState(false); - const [dimensions, setDimensions] = useState({ width: 1920, height: 400 }); + const [dimensions, setDimensions] = useState({ + width: DEFAULT_FOOTER_WIDTH, + height: DEFAULT_FOOTER_HEIGHT, + }); const [isClient, setIsClient] = useState(false); // Prevent SSR issues with canvas/animations - const [particleConfigs, setParticleConfigs] = useState([]); + const [particleConfigs, setParticleConfigs] = useState( + [], + ); // Mouse position tracking with spring physics for smooth movement const mouseX = useMotionValue(50); const mouseY = useMotionValue(50); - const smoothX = useSpring(mouseX, { damping: 50, stiffness: 100 }); - const smoothY = useSpring(mouseY, { damping: 50, stiffness: 100 }); + const smoothX = useSpring(mouseX, { + damping: SPRING_DAMPING, + stiffness: SPRING_STIFFNESS, + }); + const smoothY = useSpring(mouseY, { + damping: SPRING_DAMPING, + stiffness: SPRING_STIFFNESS, + }); const motionColoursRef = useRef>({}); + // Initialize particles on client-side only (prevents hydration mismatch) useEffect(() => { setIsClient(true); - const particlecolors = [ - cssVarAsHSL("--light-1", 0.5), - cssVarAsHSL("--light-2", 0.4), - cssVarAsHSL("--light-alt", 0.4), - // cssVarAsHSL("--logo-blue-2", 0.4), + // British spelling: particlecolours + const particlecolours = [ + cssVarAsHSL("--light-1", PARTICLE_COLOUR_ALPHA_LIGHT_1), + cssVarAsHSL("--light-2", PARTICLE_COLOUR_ALPHA_LIGHT_2), + cssVarAsHSL("--light-alt", PARTICLE_COLOUR_ALPHA_LIGHT_ALT), ]; setParticleConfigs( - Array.from({ length: 22 }, () => ({ + Array.from({ length: PARTICLE_COUNT }, () => ({ baseX: Math.random() * 100, baseY: Math.random() * 100, - size: 2 + Math.random() * 3, - delay: Math.random() * 4, - duration: 3 + Math.random() * 3, + size: PARTICLE_SIZE_MIN + Math.random() * PARTICLE_SIZE_MAX, + delay: Math.random() * PARTICLE_DELAY_MAX, + duration: + PARTICLE_DURATION_MIN + Math.random() * PARTICLE_DURATION_MAX, color: - particlecolors[Math.floor(Math.random() * particlecolors.length)], + particlecolours[ + Math.floor(Math.random() * particlecolours.length) + ], })), ); - // unfortunatly, motion cannot animate named colours. - // one readable solution is to construct a handful of constants on the client instead. + // Unfortunately, motion cannot animate named colours. + // One readable solution is to construct a handful of constants on the client instead. motionColoursRef.current = { - mouseGradStart: cssVarAsHSL("--light-alt", 0.3), - mouseGradEnd: cssVarAsHSL("--light-2", 0.3), - // radial-gradient(at 20% 30%, hsl(--light-2 / 0.3) 0px, transparent 50%), - // radial-gradient(at 80% 70%, hsl(--light-alt / 0.3) 0px, transparent 50%), - // radial-gradient(at 50% 50%, hsl(--logo-blue-1 / 0.2) 0px, transparent 50%)`, - socialBGHov: cssVarAsHSL("--light-1", 0.1), - socialBorderHov: cssVarAsHSL("--light-alt", 0.5), + mouseGradStart: cssVarAsHSL( + "--light-alt", + MOTION_COLOUR_MOUSE_GRAD_START_ALPHA, + ), + mouseGradEnd: cssVarAsHSL( + "--light-2", + MOTION_COLOUR_MOUSE_GRAD_END_ALPHA, + ), + ...createSocialMotionColours(), }; }, []); @@ -301,43 +371,7 @@ export default function Footer() { mouseY.set(((e.clientY - rect.top) / rect.height) * 100); setIsHovering(true); }; - // ideally this should pull from some source of information with the nav, but - // it's not that big of a deal - const mainLinks = [ - { label: "Home", href: "/", icon: }, - { label: "About Us", href: "/about", icon: }, - { - label: "Events", - href: "/events", - icon: , - }, - { - label: "Games Showcase", - href: "/games", - icon: , - }, - { - label: "Artwork", - href: "/artwork", - icon: , - }, - ]; - // these should be stored elsewhere... like in a data directory so - // they can be easily changed - const socialLinks = [ - // we can easily infer the domain - { url: "https://discord.com", label: "Discord" }, - { url: "https://twitter.com", label: "X (Twitter)" }, - { url: "https://github.com", label: "GitHub" }, - { url: "https://youtube.com", label: "YouTube" }, - ]; - const quickLinks = [ - { label: "Join the Club", href: "#" }, - { label: "Submit Your Game", href: "#" }, - { label: "Upcoming Jams", href: "#" }, - { label: "Resources", href: "#" }, - ]; return (