Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Projects #10

Merged
merged 23 commits into from
Nov 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added public/galaxy-circle.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 21 additions & 0 deletions public/galaxy_circle.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions public/planner_card.svg
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/projects/jupiter.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/projects/planner.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/projects/skedge.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/stars.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
63 changes: 63 additions & 0 deletions src/components/Carousel.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useCallback, forwardRef } from 'react';

type CarouselProps<T extends any[]> = {
data: T;
keyBase: string;
children: (
item: T[number],
index: number,
valueCount: number,
prev: () => void,
next: () => void,
) => JSX.Element;
};

function Carousel<T extends any[]>(
{ data, keyBase, children }: CarouselProps<T>,
ref: React.Ref<HTMLDivElement>,
) {
const generateCardKey = useCallback((idx: number) => `${keyBase}-${idx}`, [keyBase]);

const next = useCallback(
(index: number) => () => {
const el = document.querySelector(
`#${generateCardKey(Math.min(data.length - 1, index + 1))}`,
);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
},
[data.length, generateCardKey],
);

const prev = useCallback(
(index: number) => () => {
const el = document.querySelector(
`#${generateCardKey(Math.min(data.length - 1, index - 1))}`,
);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
},
[data.length, generateCardKey],
);

return (
<div
className="flex overflow-x-scroll text-white scroll-smooth no-scrollbar w-screen"
style={{ scrollSnapType: 'x mandatory' }}
ref={ref}
>
{data.map((item, idx) => {
const key = generateCardKey(idx);
return (
<div
id={key}
key={key}
className="flex flex-shrink-0 w-screen px-8 snap-start lg:px-32 xl:px-48"
>
{children(item, idx, data.length, prev(idx), next(idx))}
</div>
);
})}
</div>
);
}

export default forwardRef(Carousel);
213 changes: 213 additions & 0 deletions src/components/Projects.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import ArrowButton from '@/../public/testimonials/arrow-button.svg';
import { createRef, useEffect, useState } from 'react';
import Image, { StaticImageData } from 'next/image';
import Carousel from './Carousel';
import Planner from '@/../public/projects/planner.png';
import API from '@/../public/projects/skedge.png';
import Skedge from '@/../public/projects/skedge.png';
import Jupiter from '@/../public/projects/jupiter.png';
import FilledChevronUp from '@/../public/filled-chevron-up.svg';
import clsx from 'clsx';

type Project = {
title: string;
shortName: string;
description: string;
url: string;
image?: StaticImageData;
alt: string;
color: string;
};

const PROJECTS_INFO: Project[] = [
{
title: 'Planner',
shortName: 'Planner',
description:
'Planner is a student-focused tool for creating and tracking degree plans, offering a user-friendly interface to simplify course mapping and progress tracking.',
url: '/',
image: Planner,
alt: "A laptop displaying Planner's dashboard, showing a list of degree plan cards",
color: '#523DFF',
},
{
title: 'Sk.edge/Trends',
shortName: 'Sk.edge/Trends',
description:
'Sk.edge and Trends are tools designed to simplify the course selection and registration process by providing students with valuable data, all in one place.',
url: '/',
image: Skedge,
alt: "A mockup of Sk.edge's dashboard, displaying statistics for a professor",
color: '#6366F1',
},
{
title: 'Nebula API',
shortName: 'API',
description:
'The Nebula API serves as the authoritative data source for UTD information such as courses, student organizations, and more.',
url: '/',
// image: API,
alt: '',
color: '#FF6B4A',
},
{
title: 'Jupiter',
shortName: 'Jupiter',
description:
'Jupiter is the best way to get involved on campus. It’s easy to discover new organizations or exciting events to make the most of the on-campus experience.',
url: '/',
image: Jupiter,
alt: "A laptop displaying Jupiter's dashboard, with a list of clubs and organizations.",
color: '#926FDB',
},
];

export default function Projects() {
const [selected, setSelected] = useState(0);
const carouselRef = createRef<HTMLDivElement>();

useEffect(() => {
if (!carouselRef.current) return;
const cb: IntersectionObserverCallback = (entries) => {
if (entries.length === 0) return;
const sorted = entries.sort((a, b) => b.intersectionRatio - a.intersectionRatio);
const current = sorted[0];
setSelected(parseInt(current.target.id.split('-')[1]));
};

const obs = new IntersectionObserver(cb, {
root: carouselRef.current,
threshold: 1.0,
});

for (const child of carouselRef.current.children) {
obs.observe(child);
}

return () => {
obs.disconnect();
};
}, [carouselRef]);
const carouselKeyBase = 'projects';
return (
<div className="w-screen overflow-x-clip">
<div className="bg-stars bg-cover rounded-full flex items-center justify-center w-screen aspect-square lg:scale-[115%] scale-[200%] lg:my-52 my-[34rem] shadow-[0px_0px_87px_-1px_#312E81]">
<div className="shrink-0 flex flex-col justify-center lg:scale-[calc(1/1.15)] scale-[calc(1/2)] h-min w-screen">
<div className="text-center flex flex-col items-center">
<h3 className="text-2xl md:text-4xl text-white">Check Out Our </h3>
<h1 className="font-kallisto md:text-7xl text-4xl font-bold text-transparent w-min bg-clip-text bg-gradient-to-r from-[#6166FA] via-[#C2C9FF] to-[#FE8164]">
Projects
</h1>
</div>
<div className="text-center pt-4 text-white px-4">
<p>Check out what we have been creating in our lab up in the galaxy</p>
</div>
<div className="gap-8 grid-cols-4 mx-auto pt-6 text-white hidden lg:grid">
{PROJECTS_INFO.map((project, index) => (
<button
type="button"
key={`project-selector-${index}`}
className={`hover:scale-105 active:scale-95 transition flex h-16 px-10 justify-center items-center rounded-full cursor-pointer ${
selected === index ? 'bg-[#6166FA] border-black' : 'border-white'
} transition duration-300 ease-in-out border-2`}
onClick={() => {
const el = document.querySelector(`#${carouselKeyBase}-${index}`);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
}}
>
{project.shortName}
</button>
))}
</div>
<div className="pt-6">
<Carousel data={PROJECTS_INFO} keyBase={carouselKeyBase} ref={carouselRef}>
{(project, index, valueCount, prev, next) => (
<ProjectCard
valueCount={valueCount}
prev={prev}
next={next}
index={index}
project={project}
/>
)}
</Carousel>
</div>
</div>
</div>
</div>
);
}

function ProjectCard(props: {
index: number;
project: Project;
valueCount: number;
prev: () => void;
next: () => void;
}) {
const { project, index, next, valueCount, prev } = props;
return (
<div
className={clsx(
'rounded-3xl border w-full border-white text-white gap-8 flex-shrink-0 p-10 relative overflow-clip md:items-center',
project.image
? 'grid md:grid-cols-2 grid-cols-1'
: 'flex flex-col items-start justify-between',
)}
>
{project.image ? (
<Image src={project.image} alt={project.alt} className="md:order-2" />
) : (
<span></span>
)}

<div className={clsx('flex flex-col gap-8 md:order-1', !project.image && 'w-full')}>
<h1 className="font-kallisto text-xl md:text-3xl">{project.title}</h1>
<p className="md:text-lg text-base">{project.description}</p>

<a className="text-base md:text-lg font-bold underline flex gap-2" href={project.url}>
More Information
<Image src={FilledChevronUp} alt="" className="rotate-90" />
</a>
</div>

<span
style={{ backgroundColor: '#ffffff' }}
className={clsx(
'absolute -z-20 -top-[66px] -left-[50px] w-72 aspect-square overflow-clip rounded-full shrink-0 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)]',
)}
/>
<span
style={{ backgroundColor: project.color }}
className={clsx(
'absolute -z-10 -top-10 -left-[50px] w-72 aspect-square overflow-clip rounded-full shrink-0 blur-[75px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)]',
)}
/>

<span
style={{ backgroundColor: '#ffffff' }}
className={clsx(
'absolute -z-20 -bottom-10 -right-36 w-72 aspect-square overflow-clip rounded-full shrink-0 shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)]',
)}
/>
<span
style={{ backgroundColor: project.color }}
className={clsx(
'absolute -z-10 bottom-0 -right-36 w-72 aspect-square overflow-clip rounded-full shrink-0 blur-[75px] shadow-[0px_4px_4px_0px_rgba(0,0,0,0.25)]',
)}
/>

<span className="flex gap-3 items-center mt-auto md:mt-0 md:order-3 mr-auto place-self-end">
<button onClick={prev} className="hover:scale-105 active:scale-95 transition">
<Image src={ArrowButton} alt="arrow" />
</button>
<p className="h-min">
{index + 1}/{valueCount}
</p>
<button onClick={next} className="hover:scale-105 active:scale-95 transition">
<Image src={ArrowButton} alt="arrow" className="rotate-180" />
</button>
</span>
</div>
);
}
70 changes: 26 additions & 44 deletions src/components/Testimonials.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ArrowButton from '@/../public/testimonials/arrow-button.svg';
import Amrit from '@/../public/testimonials/amrit.png';
import JC from '@/../public/testimonials/jc.png';
import Kevin from '@/../public/testimonials/kevin.png';
import Carousel from './Carousel';

const testimonials = [
{
Expand All @@ -28,64 +29,45 @@ const testimonials = [
},
];

const Testimonials = () => (
<>
<div className="flex flex-col gap-8 text-center items-center mx-auto mt-24 my-12 max-w-3xl px-4">
<h4 className="text-gradient font-semibold lg:font-bold text-4xl">
We Got Something For Everyone
</h4>
<p>
Our organization has something for everyone. Whether you&apos;re a seasoned pro or just
starting out, we offer a range of opportunities to help you grow and develop.
</p>
</div>
<div
className="flex overflow-x-scroll text-white scroll-smooth"
style={{ scrollSnapType: 'x mandatory' }}
>
{testimonials.map((t, idx, arr) => (
<div
id={`testimonial-${idx}`}
key={`testimonial-${t.name}`}
className="flex flex-shrink-0 w-screen px-8 snap-start lg:px-32 xl:px-48 relative"
>
const Testimonials = () => {
return (
<>
<div className="flex flex-col gap-8 text-center items-center mx-auto mt-24 my-12 max-w-3xl px-4">
<h4 className="text-gradient font-semibold lg:font-bold text-4xl">
We Got Something For Everyone
</h4>
<p>
Our organization has something for everyone. Whether you&apos;re a seasoned pro or just
starting out, we offer a range of opportunities to help you grow and develop.
</p>
</div>
<Carousel data={testimonials} keyBase="testimonials">
{(item, idx, valueCount, prev, next) => (
<div className="bg-royal rounded-3xl flex-shrink-0 flex flex-col md:flex-row w-fit items-center md:items-center text-center gap-8 p-8 font-medium md:text-lg md:justify-start">
<Image className="md:w-2/5 lg:w-1/5" src={t.image} alt={t.name} />
<Image className="md:w-2/5 lg:w-1/5" src={item.image} alt={item.name} />
<span className="contents md:flex flex-col md:text-left gap-8 md:h-full md:justify-center relative">
<p>{t.quote}</p>
<p>{item.quote}</p>
<h3 className="text-2xl font-bold">
{t.name}, {t.classification}
{item.name}, {item.classification}
</h3>

<span className="flex gap-3 mt-auto mb-8 md:mb-0 md:mt-0 place-self-end md:absolute md:bottom-0 items-center">
<button
onClick={() => {
const el = document.querySelector(`#testimonial-${Math.max(0, idx - 1)}`);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
}}
>
<button onClick={prev}>
<Image src={ArrowButton} alt="arrow" />
</button>
<p className="h-min">
{idx + 1}/{arr.length}
{idx + 1}/{valueCount}
</p>
<button
onClick={() => {
const el = document.querySelector(
`#testimonial-${Math.min(arr.length - 1, idx + 1)}`,
);
el?.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'start' });
}}
>
<button onClick={next}>
<Image src={ArrowButton} alt="arrow" className="rotate-180" />
</button>
</span>
</span>
</div>
</div>
))}
</div>
</>
);
)}
</Carousel>
</>
);
};

export default Testimonials;
6 changes: 3 additions & 3 deletions src/pages/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import WhoWeAre from '@/components/WhoWeAre';
import useBlobBg from '@/lib/useBlobBg';
import CTA from '@/components/CTA';
import Testimonials from '@/components/Testimonials';
import Projects from '@/components/Projects';

const Header = () => {
const [frames, bgStyles] = useBlobBg();
Expand Down Expand Up @@ -49,11 +50,10 @@ const Home = () => (
<div>
<Header />
<WhoWeAre />
{/* <div>projects</div> */}
{/* <div>projects</div> */}
<Projects />
<Testimonials />
{/* <div>beliefs</div> */}
<CTA />
{/* <div>cta card</div> */}
<Footer />
</div>
);
Expand Down
Loading