Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
110 changes: 110 additions & 0 deletions src/components/Shop/PointsBreakdownTable.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import React from "react";

interface PointRow {
activity: string;
points: string;
note?: string;
}

interface PointCategory {
label: string;
rows: PointRow[];
}

const categories: PointCategory[] = [
{
label: "Meetings",
rows: [
{ activity: "Attend in-person GBM", points: "10 pts" },
{ activity: "Attend online GBM", points: "~5 pts" },
{ activity: "Attend lounge hours", points: "5 pts" },
],
},
{
label: "Participation",
rows: [
{ activity: "Ask questions / interact", points: "1 pt", note: "per time" },
{ activity: "Bonus participation", points: "5 pts" },
{ activity: "Submit workshop challenge", points: "20 pts" },
],
},
{
label: "Discord",
rows: [
{ activity: "General engagement", points: "2 pts", note: "per mo · max 10/sem" },
{ activity: "Helpful post / solve query", points: "5 pts", note: "max 15/sem" },
],
},
];

const PointsBreakdownTable: React.FC = () => {
return (
<div className="bg-black/40 backdrop-blur-xl shadow-2xl text-gray-300 w-full rounded-xl overflow-hidden border border-white/10">
{/* 2-D Table */}
<table className="w-full text-sm border-collapse">
<caption className="sr-only">
Points breakdown by category, activity, and points
</caption>
<thead>
<tr className="bg-white/[0.06]">
<th
scope="col"
className="text-left px-3 py-2.5 text-gray-400 font-semibold uppercase tracking-widest text-[10px] border-b border-r border-white/10 w-[24%]"
>
Category
Comment on lines +44 to +54
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

For better table accessibility, consider adding an (sr-only) <caption> describing the table and using scope="col" on the header cells so screen readers can correctly associate headers with data cells.

Copilot uses AI. Check for mistakes.
</th>
<th
scope="col"
className="text-left px-3 py-2.5 text-gray-400 font-semibold uppercase tracking-widest text-[10px] border-b border-r border-white/10"
>
Activity
</th>
<th
scope="col"
Comment on lines +59 to +63
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

The table rows use an index-based key (${ci}-${ri}), which can lead to unnecessary remounts if categories/rows are reordered. Prefer a stable key derived from the data (e.g., cat.label + row.activity, or add an explicit id).

Copilot uses AI. Check for mistakes.
className="text-center px-3 py-2.5 text-gray-400 font-semibold uppercase tracking-widest text-[10px] border-b border-white/10 w-[20%]"
>
Pts
</th>
</tr>
</thead>
<tbody>
{categories.map((cat, ci) =>
cat.rows.map((row, ri) => (
<tr
key={`${ci}-${ri}`}
className="border-b border-white/[0.05] last:border-0 hover:bg-white/[0.04] transition-colors"
>
{/* Category cell – only on first row of each category */}
{ri === 0 ? (
<td
rowSpan={cat.rows.length}
className="px-3 py-3 align-middle border-r border-white/10 text-[11px] font-semibold text-gray-400 uppercase tracking-widest leading-snug"
>
{cat.label}
</td>
) : null}

{/* Activity */}
<td className="px-3 py-3 border-r border-white/10 text-gray-200 leading-snug">
{row.activity}
{row.note && (
<span className="block text-gray-500 text-[11px] mt-0.5">{row.note}</span>
)}
</td>

{/* Points badge */}
<td className="px-3 py-3 text-center">
<span className="inline-block bg-blue-500/20 border border-blue-400/30 text-blue-300 font-bold text-xs px-2 py-1 rounded-md tabular-nums whitespace-nowrap">
{row.points}
</span>
</td>
</tr>
))
)}
</tbody>
</table>
</div>
);
};

export default PointsBreakdownTable;
111 changes: 64 additions & 47 deletions src/components/Shop/ProductCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,25 @@ interface ProductCarouselProps {
slides: CarouselSlide[];
autoplayDelay?: number;
videoUrl?: string;
rightPanel?: React.ReactNode;
}

const ProductCarousel: React.FC<ProductCarouselProps> = ({
slides,
autoplayDelay = 5000,
videoUrl = "https://framerusercontent.com/assets/sRXQsZpCuTpukMUfotGcRUuvg.mp4",
rightPanel,
}) => {
const [currentIndex, setCurrentIndex] = useState(0);
const [videoLoaded, setVideoLoaded] = useState(false);
const [videoError, setVideoError] = useState(false);
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024);

useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 1024);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
Comment on lines +27 to +32
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

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

isMobile is initialized by reading window.innerWidth during render. This will throw in any non-browser runtime (e.g., SSR/prerendering or node-based tooling). Consider guarding with typeof window !== "undefined" (or initializing to a safe default and setting it inside useEffect/matchMedia).

Suggested change
const [isMobile, setIsMobile] = useState(() => window.innerWidth < 1024);
useEffect(() => {
const handleResize = () => setIsMobile(window.innerWidth < 1024);
window.addEventListener("resize", handleResize);
return () => window.removeEventListener("resize", handleResize);
const [isMobile, setIsMobile] = useState(false);
useEffect(() => {
if (typeof window === "undefined") {
return;
}
const handleResize = () => setIsMobile(window.innerWidth < 1024);
// Set initial value on mount in the browser
handleResize();
window.addEventListener("resize", handleResize);
return () => {
window.removeEventListener("resize", handleResize);
};

Copilot uses AI. Check for mistakes.
}, []);

useEffect(() => {
if (slides.length <= 1) return;
Expand Down Expand Up @@ -63,57 +72,65 @@ const ProductCarousel: React.FC<ProductCarouselProps> = ({
</div>

{/* Carousel Content */}
<div className="relative w-full h-[350px] md:h-[450px] lg:h-[500px] z-10 pt-24 md:pt-32">
{/* Animated Text Content */}
<div className="relative w-full h-auto lg:h-[500px] z-10 pt-20 pb-8 lg:pt-32 lg:pb-0">
<div className="relative z-10 h-full flex items-center">
<div className="container mx-auto px-4 md:px-8 lg:px-16">
<div className="max-w-3xl">
<AnimatePresence mode="wait">
<motion.div
key={currentSlide.id}
initial={{ opacity: 0, x: -60, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: 60, scale: 0.95 }}
transition={{ duration: 1, ease: [0.22, 1, 0.36, 1] }}
>
{currentSlide.subtitle && (
<motion.p
className="text-blue-400 font-semibold text-xs md:text-sm lg:text-base uppercase tracking-wider mb-2 md:mb-3"
initial={{ opacity: 0, x: -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2, duration: 0.8, ease: "easeOut" }}
>
{currentSlide.subtitle}
</motion.p>
)}
<motion.h2
className="text-3xl md:text-5xl lg:text-7xl xl:text-8xl font-bold text-white mb-2 md:mb-3 leading-tight"
initial={{ opacity: 0, x: -50, rotateX: -15 }}
animate={{ opacity: 1, x: 0, rotateX: 0 }}
transition={{ delay: 0.3, duration: 0.9, ease: [0.22, 1, 0.36, 1] }}
<div className="container mx-auto px-4 md:px-8 lg:px-16 w-full">
<div className="flex flex-col items-center gap-8 lg:flex-row lg:justify-center lg:gap-24 xl:gap-48">
{/* Left: animated text */}
<div className="w-full lg:w-72 xl:w-[408px] lg:shrink-0">
<AnimatePresence mode="wait">
<motion.div
key={currentSlide.id}
initial={{ opacity: 0, x: isMobile ? 0 : -60, scale: 0.95 }}
animate={{ opacity: 1, x: 0, scale: 1 }}
exit={{ opacity: 0, x: isMobile ? 0 : 60, scale: 0.95 }}
transition={{ duration: 1, ease: [0.22, 1, 0.36, 1] }}
>
{currentSlide.title}
</motion.h2>
{currentSlide.description && (
<motion.p
className="text-gray-300 text-sm md:text-lg lg:text-2xl mb-6 md:mb-10 max-w-2xl"
initial={{ opacity: 0, x: -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5, duration: 0.8, ease: "easeOut" }}
{currentSlide.subtitle && (
<motion.p
className="text-blue-400 font-semibold text-xs md:text-sm uppercase tracking-wider mb-2"
initial={{ opacity: 0, x: isMobile ? 0 : -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.2, duration: 0.8, ease: "easeOut" }}
>
{currentSlide.subtitle}
</motion.p>
)}
<motion.h2
className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-bold text-white mb-3 leading-tight"
initial={{ opacity: 0, x: isMobile ? 0 : -50, rotateX: -15 }}
animate={{ opacity: 1, x: 0, rotateX: 0 }}
transition={{ delay: 0.3, duration: 0.9, ease: [0.22, 1, 0.36, 1] }}
>
{currentSlide.description}
</motion.p>
)}
</motion.div>
</AnimatePresence>
{currentSlide.title}
</motion.h2>
{currentSlide.description && (
<motion.p
className="text-gray-300 text-sm md:text-base mb-6 md:mb-8 max-w-sm"
initial={{ opacity: 0, x: isMobile ? 0 : -40 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5, duration: 0.8, ease: "easeOut" }}
>
{currentSlide.description}
</motion.p>
)}
</motion.div>
</AnimatePresence>

<a
href="#products"
className="inline-block bg-blue-500/10 backdrop-blur-xl border border-blue-400/20 hover:bg-blue-500/20 hover:border-blue-400/40 text-white font-semibold px-6 py-3 rounded-lg transition-all duration-300 hover:scale-105 shadow-lg shadow-blue-500/10 text-sm md:text-base"
>
Shop Now
</a>
</div>

{/* Static CTA Button */}
<a
href="#products"
className="inline-block bg-blue-500/10 backdrop-blur-xl border border-blue-400/20 hover:bg-blue-500/20 hover:border-blue-400/40 text-white font-semibold px-6 py-3 md:px-10 md:py-4 rounded-lg md:rounded-xl transition-all duration-300 hover:scale-105 shadow-lg shadow-blue-500/10 text-sm md:text-base lg:text-lg"
>
Shop Now
</a>
{/* Right: optional panel (e.g. points table) */}
{rightPanel && (
<div className="w-full max-h-[45vh] overflow-y-auto lg:max-h-none lg:overflow-visible lg:w-[440px] lg:shrink-0">
{rightPanel}
</div>
)}
</div>
</div>
</div>
Expand Down
9 changes: 7 additions & 2 deletions src/pages/Shop/ShopIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { CategoryLayout } from "../../components/Shop/CategoryLayout";
import { useProducts } from "../../hooks/useProducts";
import { motion, useInView } from "framer-motion";
import LoadingSpinner from "../../components/LoadingSpinner";
import PointsBreakdownTable from "../../components/Shop/PointsBreakdownTable";
import "./styles/scrolling-text.css";

const ShopIndex: React.FC = () => {
Expand Down Expand Up @@ -62,9 +63,13 @@ const ShopIndex: React.FC = () => {
</Helmet>
<div className="min-h-screen bg-black text-white h-screen overflow-y-scroll snap-y snap-mandatory snap-container">
{/* Hero Carousel Section - Full Screen Snap */}
<div className="snap-section relative overflow-hidden flex flex-col">
<div className="snap-section relative lg:overflow-hidden flex flex-col">
<div className="flex-1 flex flex-col justify-center">
<ProductCarousel slides={carouselSlides} autoplayDelay={5000} />
<ProductCarousel
slides={carouselSlides}
autoplayDelay={5000}
rightPanel={<PointsBreakdownTable />}
/>
</div>

{/* Scroll indicator */}
Expand Down
Loading