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

Feat/organizer carousel #535

Merged
merged 12 commits into from
Jan 26, 2025
2 changes: 2 additions & 0 deletions apps/sanity/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import resource from "./resource";
import resourceCategory from "./resourceCategory";
import resourceCategoryOrder from "./resourceCategoryOrder";
import sponsors from "./sponsors";
import organizers from "./organizers";

export const schemaTypes = [
faqs,
Expand All @@ -14,4 +15,5 @@ export const schemaTypes = [
resourceCategory,
resourceCategoryOrder,
sponsors,
organizers,
];
73 changes: 73 additions & 0 deletions apps/sanity/schemas/organizers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import { defineType, defineField, defineArrayMember } from "sanity";
import { Users } from "lucide-react";

export default defineType({
name: "organizers",
title: "Organizers",
icon: Users,
type: "document",
fields: [
defineField({
name: "organizers",
title: "Organizers",
type: "array",
of: [
defineArrayMember({
type: "object",
name: "organizer",
fields: [
defineField({
name: "name",
title: "Name",
type: "string",
validation: (Rule) => Rule.required(),
}),
defineField({
name: "department",
title: "Department",
type: "string",
validation: (Rule) => Rule.required(),
options: {
list: [
{ title: "Tech", value: "Tech" },
{ title: "Marketing", value: "Marketing" },
{ title: "Logistics", value: "Logistics" },
{ title: "Corporate", value: "Corporate" },
{ title: "Graphics", value: "Graphics" },
],
},
}),
defineField({
name: "role",
title: "Role",
type: "string",
validation: (Rule) => Rule.required(),
options: {
list: [
{ title: "Director", value: "Director" },
{ title: "Organizer", value: "Organizer" },
{ title: "Intern", value: "Intern" },
{ title: "Advisor", value: "Advisor" },
],
},
}),
defineField({
name: "image",
title: "Profile Image",
type: "image",
options: {
hotspot: true,
},
}),
defineField({
name: "link",
title: "Social Link",
type: "url",
description: "LinkedIn or other social media profile link",
}),
],
}),
],
}),
],
});
13 changes: 9 additions & 4 deletions apps/site/src/app/(main)/(home)/page.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
import { Landing, ChooseCharacter, FAQ, Sponsors, Partners } from "./sections";
import {
Landing,
ChooseCharacter,
FAQ,
Sponsors,
Partners,
Organizers,
} from "./sections";

export const revalidate = 60;

Expand All @@ -8,9 +15,6 @@ export default function Home() {
return process.env.MAINTENANCE_MODE_HOME ? (
<>
<Landing />
<ChooseCharacter />
<FAQ />
<Sponsors />
</>
) : (
<>
Expand All @@ -19,6 +23,7 @@ export default function Home() {
<FAQ />
<Sponsors />
<Partners />
<Organizers />
</>
);
}
64 changes: 64 additions & 0 deletions apps/site/src/app/(main)/(home)/sections/InfiniteMovingCards.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use client";

import React, { useEffect, useState } from "react";

export const InfiniteMovingCards = ({
items,
}: {
items: {
quote: string;
name: string;
title: string;
image: string;
link: string;
}[];
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
className?: string;
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLUListElement>(null);
const [start, setStart] = useState(false);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <@typescript-eslint/no-unused-vars> reported by reviewdog 🐶
'start' is assigned a value but never used.


useEffect(() => {
if (!containerRef.current || !scrollerRef.current) return;

// Clear existing clones
while (scrollerRef.current.children.length > items.length) {
scrollerRef.current.lastChild &&
scrollerRef.current.removeChild(scrollerRef.current.lastChild);
}

// Clone items just once for infinite scroll
const originalItems = Array.from(scrollerRef.current.children).slice(
0,
items.length,
);
originalItems.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem);
}
});

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setStart(true);
}
});
},
{ threshold: 0.1 },
);

observer.observe(containerRef.current);

return () => {
observer.disconnect();
};
}, [items]);

// ... rest of the component stays the same ...
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
"use client";

import React, { useEffect, useState } from "react";
import box from "@/assets/images/center_chat_box.svg";
import boxBG from "@/assets/images/center_chat_box_bg.svg";
import Link from "next/link";
import Image from "next/image";

export const InfiniteMovingCards = ({
items,
direction = "left",
speed = "fast",
pauseOnHover = true,
}: {
items: {
quote: string;
name: string;
title: string;
image: string;
link: string;
}[];
direction?: "left" | "right";
speed?: "fast" | "normal" | "slow";
pauseOnHover?: boolean;
className?: string;
}) => {
const containerRef = React.useRef<HTMLDivElement>(null);
const scrollerRef = React.useRef<HTMLUListElement>(null);
const [start, setStart] = useState(false);

useEffect(() => {
if (!containerRef.current || !scrollerRef.current) return;

// Clone items multiple times to ensure smooth infinite scroll
const scrollerContent = Array.from(scrollerRef.current.children);
// Clone enough times to ensure continuous scroll
scrollerContent.forEach((item) => {
const duplicatedItem = item.cloneNode(true);
if (scrollerRef.current) {
scrollerRef.current.appendChild(duplicatedItem);
}
});

const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
setStart(true);
}
});
},
{ threshold: 0.1 },
);

observer.observe(containerRef.current);

return () => {
observer.disconnect();
};
}, [items]);

const duration =
speed === "fast" ? "30s" : speed === "normal" ? "45s" : "270s";

return (
<div
ref={containerRef}
className="scroller relative z-[20] max-w-3xl overflow-hidden [mask-image:linear-gradient(to_right,transparent,white_20%,white_80%,transparent)] pt-28"
style={
{
"--duration": duration,
} as React.CSSProperties
}
>
<ul
ref={scrollerRef}
className={`flex min-w-full shrink-0 gap-2 py-4 w-max flex-nowrap items-center justify-center ${
start ? "animate-scroll" : ""
} ${pauseOnHover ? "hover:[animation-play-state:paused]" : ""} ${
direction === "right" ? "[animation-direction:reverse]" : ""
}`}
>
{items.map((item, idx) => (
<li
key={`${item.name}-${idx}`}
className="relative flex-shrink-0 group"
style={{ transform: "skew(-20deg)" }}
>
{/* Info popup on hover */}
<div className="absolute -top-[110px] left-[12%] -translate-x-1/2 opacity-0 group-hover:opacity-100 z-[300] pointer-events-none transition-opacity">
<div
className="relative w-[200px] h-[100px]"
style={{
transform: "skew(20deg)",
}}
>
{/* Background chat box - positioned slightly offset */}
<div className="absolute inset-0 -right-1 -bottom-1">
<Image
src={boxBG}
alt="Box Background"
className="w-full h-full object-contain"
style={{ maxWidth: "100%" }}
/>
</div>

{/* Main chat box */}
<div className="absolute inset-0">
<Image
src={box}
alt="Box"
className="w-full h-full object-contain"
style={{ maxWidth: "100%" }}
/>
</div>

<div className="absolute inset-0 flex flex-col items-center justify-center gap-0">
<p className="text-sm font-medium text-gray-200 leading-tight">
{item.name}
</p>
<p className="text-xs text-gray-400 leading-tight">
{item.title}
</p>
</div>
</div>
</div>

{/* Card */}
<div className="relative w-16 h-20 bg-black border-2 border-white overflow-hidden transition-transform group-hover:scale-105 group-hover:bg-[#006FB2]">
<div
className="absolute inset-0 bg-black group-hover:bg-[#006FB2]"
style={{ transform: "skew(20deg) scale(1.2)" }}
>
<Link
href={item.link}
target="_blank"
className="relative block w-full h-full"
prefetch={false}
>
<img
src={item.image}
alt={item.name}
className="object-cover w-full h-full opacity-75 group-hover:opacity-100"
style={{ maxWidth: "100%" }}
/>
</Link>
</div>
</div>
</li>
))}
</ul>
</div>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
@keyframes scroll {
from {
transform: translateX(0);
}
to {
transform: translateX(calc(-50%));
}
}
Loading
Loading