Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Cat2005 committed May 30, 2024
2 parents fe869bc + de9638e commit b132e98
Show file tree
Hide file tree
Showing 12 changed files with 352 additions and 7 deletions.
14 changes: 10 additions & 4 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type { Metadata, Viewport } from 'next'
import './globals.css'
import Navbar from '@/components/Navbar'
import { prefix } from '@/utils/prefix'
import PixelBackground from '@/components/PixelBackground'

export const metadata: Metadata = {
title: 'CompSoc',
description:
"CompSoc is Edinburgh University's technology society! We're Scotland's best and largest of its kind, and form one of the largest societies within the university.",
icons: {
icon: '/compsoc-mini.png',
icon: `${prefix}/compsoc-mini.png`,
},
}

Expand All @@ -22,9 +24,13 @@ export default function RootLayout({
}>) {
return (
<html lang="en">
<body className="bg-background relative">
<Navbar />
<div className="container mx-auto max-w-screen-xl p-4">{children}</div>
<body className="relative">
<PixelBackground>
<Navbar />
<div className="container mx-auto max-w-screen-xl p-4">
{children}
</div>
</PixelBackground>
</body>
</html>
)
Expand Down
10 changes: 9 additions & 1 deletion app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"use client";

import JoinDiscord from '@/components/JoinDiscord'
import { useAnimatedBackground } from '@/context/AnimatedBackgroundContext'
import { prefix } from '@/utils/prefix'
import Image from 'next/image'
import EventSection from './eventgrid/EventSection';
Expand All @@ -9,7 +11,7 @@ import PartnerSection from './partnersection/PartnerSection';

export default function Home() {


const { toggleBackground } = useAnimatedBackground()
return (
<main className="bg-background flex items-center justify-center min-h-screen flex-col p-24">
<div className='w-full'>
Expand All @@ -25,6 +27,12 @@ export default function Home() {
'drop-shadow(0px 0px 50px rgba(255, 255, 255, 0.1)) drop-shadow(0px 0px 197.8px rgba(255, 255, 255, 0.1))',
}}
/>
<JoinDiscord />
<button
onClick={toggleBackground}
className="px-4 py-2 text-white bg-blue-500 rounded-lg shadow-lg">
TEST: Toggle background
</button>
</div>

<EventSection></EventSection>
Expand Down
150 changes: 150 additions & 0 deletions components/JoinDiscord.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
import { DISCORD_INVITE_URL } from '@/constants/discord'
import { prefix } from '@/utils/prefix'
import Image from 'next/image'
import { motion } from 'framer-motion'
import { FC, useEffect, useState } from 'react'
import prefersReducedMotion from '@/utils/prefersReducedMotion'

const PARTICLES_COUNT = Array.from({ length: 20 }, (_, i) => ({
index: i,
x: Math.random() * 500 - 250,
y: Math.random() * 300 - 150,
delay: Math.random() * 2,
duration: 2 + Math.random() * 2,
}))

const Particle: FC<{
x: number
y: number
delay: number
duration: number
isHovered: boolean
}> = ({ x, y, delay, duration, isHovered }) => {
const particleVariants = {
hidden: {
opacity: 0,
x: x,
y: y,
duration: 10,
delay: delay,
},
visible: {
opacity: [0, 0.8, 0],
x: 0,
y: 0,
transition: {
duration: 2,
delay: Math.random() * 2,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
hover: {
opacity: [0, 1, 0],
x: 1,
y: 1,
transition: {
duration: duration,
delay: Math.random() * 2,
repeat: Infinity,
repeatType: 'loop' as const,
},
},
}

return (
<motion.div
className="absolute w-2 h-2 bg-discordPurple rounded-sm"
initial="hidden"
variants={particleVariants}
// animate={isHovered ? 'hover' : 'visible'}
animate="visible" // Im sure there is a way to make the duration switch work, but it just doesn't seem to work for me + does not look good
/>
)
}

const Particles: FC<{ isHovered: boolean }> = ({ isHovered }) => {
if (prefersReducedMotion()) return null
return (
<>
{PARTICLES_COUNT.map(({ index, x, y, delay, duration }) => (
<Particle
key={index}
x={x}
y={y}
delay={delay}
duration={duration}
isHovered={isHovered}
/>
))}
</>
)
}

// TODO: If we ever run this website on server, we should cache this value!
const MemberCount: FC = () => {
const [memberCount, setMemberCount] = useState<number | null>(null)
const [onlineCount, setOnlineCount] = useState<number | null>(null)

useEffect(() => {
const fetchMemberCount = async () => {
const INVITE_CODE = DISCORD_INVITE_URL.split('/').pop()
try {
const response = await fetch(
`https://discord.com/api/v9/invites/${INVITE_CODE}?with_counts=true&with_expiration=true`
)
const data = await response.json()
setMemberCount(data.approximate_member_count)
setOnlineCount(data.approximate_presence_count)
} catch (error) {
console.error('Error fetching member count:', error)
}
}

fetchMemberCount()
}, [])

return (
<div
className="text-white text-center"
title={`There are ${onlineCount} members online`}>
<p>Join our Discord!</p>
{memberCount !== null && (
<p className="text-xs text-left text-gray-300">{memberCount} members</p>
)}
</div>
)
}

const JoinDiscord: FC = () => {
const [isHovered, setIsHovered] = useState(false)

return (
<div className="relative flex items-center justify-center">
<Particles isHovered={isHovered} />
<motion.a
href={DISCORD_INVITE_URL}
target="_blank"
className="px-4 py-2 text-white bg-discordPurple rounded-lg z-10 shadow-lg flex items-center gap-2"
whileHover={{ scale: 1.1 }}
onHoverStart={() => setIsHovered(true)}
onHoverEnd={() => setIsHovered(false)}>
{/*
I would use this icon... but its the ugliest thing I've ever seen
<Discord width={48} height={48} />
So lets use a random svg
*/}
<Image
src={`${prefix}/discord.svg`}
alt="Discord logo"
width={48}
height={40}
/>

<MemberCount />
</motion.a>
</div>
)
}

export default JoinDiscord
75 changes: 75 additions & 0 deletions components/PixelBackground.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'

import React, { useEffect, useState, ReactNode } from 'react'
import {
AnimatedBackgroundProvider,
useAnimatedBackground,
} from '@/context/AnimatedBackgroundContext'

const PixelGrid = () => {
const { isActive } = useAnimatedBackground()
const [numPixels, setNumPixels] = useState({ x: 0, y: 0 })
const [pixels, setPixels] = useState<number[]>([])
const [pixelSize, _] = useState<number>(64) // Smaller values are less performant

const calculatePixels = () => {
const x = Math.ceil(window.innerWidth / pixelSize)
const y = Math.ceil(window.innerHeight / pixelSize)
return { x, y }
}

const generatePixels = (x: number, y: number) => {
return Array(x * y)
.fill(0)
.map(() => Math.random())
}

useEffect(() => {
const { x, y } = calculatePixels()
setNumPixels({ x, y })
setPixels(generatePixels(x, y))

const handleResize = () => {
const { x, y } = calculatePixels()
setNumPixels({ x, y })
setPixels(generatePixels(x, y))
}

window.addEventListener('resize', handleResize)

return () => {
window.removeEventListener('resize', handleResize)
}
}, [])

return (
<div
className="fixed pointer-events-none z-[-1] grid"
style={{ gridTemplateColumns: `repeat(${numPixels.x}, ${pixelSize}px)` }}>
{pixels.map((delay, index) => (
<div
key={index}
style={{
width: `${pixelSize}px`,
height: `${pixelSize}px`,
transitionDelay: `${delay * 750}ms`,
}}
className={`transition-colors ${
isActive ? 'bg-csred' : 'bg-background'
}`}
/>
))}
</div>
)
}

const PixelBackground = ({ children }: { children: ReactNode }) => {
return (
<AnimatedBackgroundProvider>
<PixelGrid />
{children}
</AnimatedBackgroundProvider>
)
}

export default PixelBackground
1 change: 1 addition & 0 deletions constants/discord.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const DISCORD_INVITE_URL = 'https://discord.gg/fmp7p9Ca4y'
41 changes: 41 additions & 0 deletions context/AnimatedBackgroundContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
'use client'
import { createContext, useContext, useState, ReactNode } from 'react'

interface AnimatedBackgroundContextProps {
isActive: boolean
toggleBackground: () => void
}

const AnimatedBackgroundContext = createContext<
AnimatedBackgroundContextProps | undefined
>(undefined)

export const useAnimatedBackground = (): AnimatedBackgroundContextProps => {
const context = useContext(AnimatedBackgroundContext)
if (!context) {
throw new Error(
'useAnimatedBackground must be used within an AnimatedBackgroundProvider'
)
}
return context
}

interface AnimatedBackgroundProviderProps {
children: ReactNode
}

export const AnimatedBackgroundProvider = ({
children,
}: AnimatedBackgroundProviderProps) => {
const [isActive, setIsActive] = useState(false)

const toggleBackground = () => {
setIsActive(!isActive)
}

return (
<AnimatedBackgroundContext.Provider value={{ isActive, toggleBackground }}>
{children}
</AnimatedBackgroundContext.Provider>
)
}
38 changes: 38 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit b132e98

Please sign in to comment.