Skip to content

Commit

Permalink
Merge pull request #33 from NUSComputingDev/KAN-28-Elections
Browse files Browse the repository at this point in the history
Add candidates manifesto and photo
  • Loading branch information
Respirayson authored Aug 19, 2024
2 parents b69ec50 + 3929dc6 commit 988cf1c
Show file tree
Hide file tree
Showing 18 changed files with 880 additions and 260 deletions.
23 changes: 23 additions & 0 deletions hooks/use-outside-click.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import React, { useEffect } from "react";

export const useOutsideClick = (
ref: React.RefObject<HTMLDivElement>,
callback: Function
) => {
useEffect(() => {
const listener = (event: any) => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
callback(event);
};

document.addEventListener("mousedown", listener);
document.addEventListener("touchstart", listener);

return () => {
document.removeEventListener("mousedown", listener);
document.removeEventListener("touchstart", listener);
};
}, [ref, callback]);
};
Binary file added public/elections/candidates/Mu_Junrong_photo.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/chendongjun.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/emelynlee.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/gowri.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/jhasatwik.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/jingxiang.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/jolyn.jpeg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/liuyuhang.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/ravi.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/ryanneo.PNG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/shananth.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added public/elections/candidates/sunjiaen.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion src/App.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Toaster } from 'react-hot-toast';
import NavigationBar from './layout/NavigationBar';
import Footer from './layout/Footer';
import './App.css';
import { Candidates } from './pages/elections/Candidates';

function App() {
return (
Expand All @@ -24,7 +25,10 @@ function App() {
<Route path=':articleLink' element={<Article />} />
</Route>
<Route path='/resources' element={<Resources />} />
<Route path='/elections' element={<Elections />} />
<Route path='/elections'>
<Route path='' element={<Elections />} />
<Route path='candidates' element={<Candidates />} />
</Route>
</Routes>
</main>

Expand Down
1 change: 1 addition & 0 deletions src/layout/NavigationMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ function NavigationMenu() {
'Events': '/events',
'Resources': '/resources',
'Elections': '/elections',
'Candidates': '/elections/candidates',
'Photos': 'https://www.flickr.com/photos/137141057@N04/albums/',
};

Expand Down
224 changes: 224 additions & 0 deletions src/pages/elections/Candidates.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
'use client';
import React, { useEffect, useId, useRef, useState } from 'react';
import { AnimatePresence, motion } from 'framer-motion';
import { useOutsideClick } from '../../../hooks/use-outside-click';
import WindowCard from '../../layout/WindowCard';
import { cards } from './constants';

export function Candidates() {
const [active, setActive] = useState<(typeof cards)[number] | boolean | null>(
null,
);
const ref = useRef<HTMLDivElement>(null);
const id = useId();

useEffect(() => {
function onKeyDown(event: KeyboardEvent) {
if (event.key === 'Escape') {
setActive(false);
}
}

if (active && typeof active === 'object') {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'auto';
}

window.addEventListener('keydown', onKeyDown);
return () => window.removeEventListener('keydown', onKeyDown);
}, [active]);

useOutsideClick(ref, () => setActive(null));

const data = cards.sort((a, b) => ('' + a.title).localeCompare(b.title)).map((card, index) => {
const content = (
<>
<div className='w-48 p-4 flex flex-col items-stretch gap-1 flex-1'>
<motion.div
className='self-center'
layoutId={`image-${card.title}-${index}-${card.id}`}
>
<img
src={card.src}
alt={card.title}
className='rounded-md w-32 h-32 object-cover object-center'
/>
</motion.div>
<div className='m-2 flex flex-col justify-between h-48'>
<div>
<motion.h3
layoutId={`title-${card.title}-${index}-${card.id}`}
className='font-medium text-neutral-800 text-center md:text-left'
>
{card.title}
</motion.h3>
<motion.p
layoutId={`description-${card.description}-${index}-${card.id}`}
className='text-neutral-600 text-center md:text-left'
>
{card.description}
</motion.p>
</div>
<motion.button
layoutId={`button-${card.title}-${index}-${card.id}`}
className='mt-4 px-4 py-2 text-sm rounded-full font-bold bg-gray-100 hover:bg-yellow-500 hover:text-white text-black'
>
{card.ctaText}
</motion.button>
</div>
</div>
</>
);

return (
<motion.div
layoutId={`card-${card.title}-${id}-${card.id}`}
key={`card-${card.title}-${id}-${card.id}`}
onClick={() => setActive(card)}
className='p-4 flex flex-row flex-wrap justify-center
items-center rounded-xl cursor-pointer'
>
<WindowCard content={content} key={index}></WindowCard>
</motion.div>
);
});

return (
<section className='elections h-full gap-4'>
<h1 className='title'>Candidates</h1>
<p className='text-neutral-500 text-xl text-center'>
See the candidates running for the NUS Students&apos; Computing Club
Elections today!
</p>
<AnimatePresence>
{active && typeof active === 'object' && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='fixed inset-0 bg-black/20 h-full w-full z-[100]'
/>
)}
</AnimatePresence>
<AnimatePresence>
{active && typeof active === 'object' ? (
<div className='fixed inset-0 grid place-items-center z-[101]'>
<motion.button
key={`button-${active.title}-${id}`}
layout
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
className='flex absolute top-2 right-2 lg:hidden items-center justify-center bg-white rounded-full h-6 w-6'
onClick={() => setActive(null)}
>
<CloseIcon />
</motion.button>
<motion.div
layoutId={`card-${active.title}-${id}-${active.id}`}
ref={ref}
className='w-full max-w-[800px] h-full sm:h-fit md:max-h-[93%] px-2 flex flex-col sm:flex-row bg-white sm:rounded-3xl overflow-auto self-center sm:overflow-hidden'
>
<motion.div
className='w-full mb-0 my-16 sm:my-auto'
layoutId={`image-${active.title}-${id}-${active.id}`}
>
<img
src={active.src}
alt={active.title}
className=' w-[500px] sm:rounded-tr-lg sm:rounded-tl-lg object-cover object-center self-center h-96 sm:h-fit rounded-lg'
/>
</motion.div>

<div className='w-full pt-8'>
<div className='flex justify-between items-start p-4'>
<div className=''>
<motion.h3
layoutId={`title-${active.title}-${id}-${active.id}`}
className='font-bold text-neutral-700'
>
{active.title}
</motion.h3>
<motion.p
layoutId={`description-${active.description}-${id}-${active.id}`}
className='text-neutral-600 overflow-auto'
>
{active.description}
</motion.p>
<motion.p
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='text-neutral-600 overflow-auto'
>
{active.year} / {active.major}
</motion.p>
</div>
</div>
<div className='relative px-4'>
<motion.div
layout
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className='text-neutral-600 text-sm h-fit sm:h-fit pb-10 flex flex-col items-start gap-4 overflow-auto'
>
{typeof active.content === 'function'
? active.content()
: active.content}
</motion.div>
</div>
</div>
</motion.div>
</div>
) : null}
</AnimatePresence>
<ul className='gap-8 flex flex-row flex-wrap justify-center items-center w-full px-8 md:px-24'>
{data}
</ul>
</section>
);
}

export const CloseIcon = () => {
return (
<motion.svg
initial={{
opacity: 0,
}}
animate={{
opacity: 1,
}}
exit={{
opacity: 0,
transition: {
duration: 0.05,
},
}}
xmlns='http://www.w3.org/2000/svg'
width='24'
height='24'
viewBox='0 0 24 24'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
strokeLinejoin='round'
className='h-4 w-4 text-black'
>
<path stroke='none' d='M0 0h24v24H0z' fill='none' />
<path d='M18 6l-12 12' />
<path d='M6 6l12 12' />
</motion.svg>
);
};
Loading

0 comments on commit 988cf1c

Please sign in to comment.