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
Binary file added public/Soda.mp4
Binary file not shown.
2 changes: 1 addition & 1 deletion src/components/Shop/CategorySection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ export const CategorySection: React.FC<CategorySectionProps> = ({
<motion.div
ref={scrollRef}
className={`absolute inset-0 flex ${orientation === "vertical" ? "flex-col justify-center" : "items-center"} overflow-hidden pointer-events-none group-hover:opacity-10 transition-opacity duration-300 ease-in-out will-change-[opacity]`}
initial={{ opacity: 0 }}
initial={{ opacity: 0.3 }}
animate={{ opacity: 0.3 }}
exit={{ opacity: 0 }}
transition={{ delay: 0.6, duration: 0.5 }}
Expand Down
34 changes: 7 additions & 27 deletions src/components/Shop/PointsBreakdownTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,38 +13,21 @@ interface PointCategory {

const categories: PointCategory[] = [
{
label: "Meetings",
label: "Events",
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" },
{ activity: "GBM", points: "10 pts" },
{ activity: "Collaboration (with other clubs)", points: "15 pts" },
{ activity: "Hackathon/Bootcamp", points: "20 pts" },
{ activity: "Lounge Hour", points: "15 pts" },
],
},
];

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>
<caption className="sr-only">Points breakdown for SoDA activities</caption>
<thead>
<tr className="bg-white/[0.06]">
<th
Expand Down Expand Up @@ -72,9 +55,8 @@ const PointsBreakdownTable: React.FC = () => {
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"
className="border-b border-white/[0.05] last:border-0 hover:bg-white/[0.04]"
>
{/* Category cell – only on first row of each category */}
{ri === 0 ? (
<td
rowSpan={cat.rows.length}
Expand All @@ -84,15 +66,13 @@ const PointsBreakdownTable: React.FC = () => {
</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}
Expand Down
111 changes: 111 additions & 0 deletions src/components/Shop/PointsDrawer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import React, { useState, useRef } from "react";
import PointsBreakdownTable from "./PointsBreakdownTable";
import { ArrowLeft, X } from "lucide-react";

/**
* Floating drawer that keeps the points table accessible across all shop pages.
* - Hover to preview, click to pin open, Escape or blur to close.
*/
const PointsDrawer: React.FC = () => {
const [isOpen, setIsOpen] = useState(false);
const [isPinned, setIsPinned] = useState(false);
const drawerRef = useRef<HTMLDivElement>(null);

const openDrawer = () => setIsOpen(true);

const closeDrawer = () => {
setIsOpen(false);
setIsPinned(false);
};

const handleMouseEnter = () => setIsOpen(true);
const handleMouseLeave = () => {
if (!isPinned) setIsOpen(false);
};

const handleTogglePin = () => {
setIsPinned((prev) => {
const next = !prev;
setIsOpen(next);
return next;
});
};

const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "Escape") closeDrawer();
};

const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
if (drawerRef.current && !drawerRef.current.contains(e.relatedTarget as Node)) {
if (!isPinned) setIsOpen(false);
}
};

const translateClass = isOpen ? "translate-x-0" : "translate-x-[calc(100%-2rem)]";

return (
<>
<div
ref={drawerRef}
className="fixed top-1/2 right-[-6px] -translate-y-1/2 z-40 group/drawer hidden md:block"
onKeyDown={handleKeyDown}
onBlur={handleBlur}
>
<div
className={`flex items-center transition-transform duration-300 ease-out ${translateClass}`}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
<button
type="button"
className="bg-blue-500/20 backdrop-blur-md text-white w-4 sm:w-5 px-0.5 py-3 border border-white/15 shadow-lg shadow-blue-500/10 text-[10px] font-semibold tracking-[0.25em] uppercase h-[23vh] flex items-center justify-between cursor-pointer select-none"
style={{ writingMode: "vertical-rl" }}
onClick={handleTogglePin}
onFocus={openDrawer}
aria-expanded={isOpen}
aria-controls="points-breakdown-desktop-panel"
>
<ArrowLeft size={20} color="grey" /> Points Breakdown{" "}
<ArrowLeft size={20} color="grey" />
</button>
<div
id="points-breakdown-desktop-panel"
className="w-[340px] max-h-[80vh] overflow-y-auto border border-white/10 rounded-l-none rounded-r-xl"
>
<PointsBreakdownTable />
</div>
</div>
</div>

<div className="fixed bottom-4 right-4 z-40 md:hidden">
{!isOpen ? (
<button
type="button"
onClick={openDrawer}
className="bg-blue-500/20 backdrop-blur-md text-white border border-white/15 shadow-lg shadow-blue-500/10 rounded-full px-4 py-2 text-xs font-semibold uppercase tracking-wider"
>
Points
</button>
) : (
<div className="w-[calc(100vw-2rem)] max-w-sm">
<div className="flex items-center justify-end mb-2">
<button
type="button"
onClick={closeDrawer}
aria-label="Close points breakdown"
className="bg-black/40 backdrop-blur-md text-white border border-white/15 rounded-full p-2"
>
<X size={16} />
</button>
</div>
<div className="max-h-[65vh] overflow-y-auto border border-white/10 rounded-xl">
<PointsBreakdownTable />
</div>
</div>
)}
</div>
</>
);
};

export default PointsDrawer;
41 changes: 29 additions & 12 deletions src/components/Shop/ProductCarousel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@ interface ProductCarouselProps {
const ProductCarousel: React.FC<ProductCarouselProps> = ({
slides,
autoplayDelay = 5000,
videoUrl = "https://framerusercontent.com/assets/sRXQsZpCuTpukMUfotGcRUuvg.mp4",
videoUrl = "/Soda.mp4",
rightPanel,
}) => {
const hasRightPanel = !!rightPanel;
const [currentIndex, setCurrentIndex] = useState(0);
const [videoLoaded, setVideoLoaded] = useState(false);
const [videoError, setVideoError] = useState(false);
Expand Down Expand Up @@ -75,9 +76,19 @@ const ProductCarousel: React.FC<ProductCarouselProps> = ({
<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 w-full">
<div className="flex flex-col items-center gap-8 lg:flex-row lg:justify-center lg:gap-24 xl:gap-48">
<div
className={`flex flex-col items-center gap-8 lg:flex-row lg:justify-center ${
hasRightPanel ? "lg:gap-24 xl:gap-48" : "lg:gap-12 xl:gap-16"
}`}
>
{/* Left: animated text */}
<div className="w-full lg:w-72 xl:w-[408px] lg:shrink-0">
<div
className={`w-full flex flex-col ${
hasRightPanel
? "lg:w-[360px] xl:w-[420px] lg:shrink-0"
: "lg:w-[960px] xl:w-[1100px] items-center text-center"
}`}
>
<AnimatePresence mode="wait">
<motion.div
key={currentSlide.id}
Expand All @@ -97,7 +108,9 @@ const ProductCarousel: React.FC<ProductCarouselProps> = ({
</motion.p>
)}
<motion.h2
className="text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-bold text-white mb-3 leading-tight"
className={`text-3xl md:text-4xl lg:text-5xl xl:text-6xl font-bold text-white mb-3 leading-tight ${
hasRightPanel ? "" : "text-center w-full"
}`}
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] }}
Expand All @@ -106,8 +119,10 @@ const ProductCarousel: React.FC<ProductCarouselProps> = ({
</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 }}
className={`text-gray-300 ${hasRightPanel ? "text-sm md:text-base max-w-sm" : "text-lg md:text-xl lg:text-2xl max-w-3xl"} mb-6 md:mb-8 ${
hasRightPanel ? "" : "text-center"
}`}
initial={{ opacity: 0, x: 0 }}
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 0.5, duration: 0.8, ease: "easeOut" }}
>
Expand All @@ -117,12 +132,14 @@ const ProductCarousel: React.FC<ProductCarouselProps> = ({
</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 className={hasRightPanel ? "" : "flex justify-center w-full"}>
<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>
</div>

{/* Right: optional panel (e.g. points table) */}
Expand Down
2 changes: 2 additions & 0 deletions src/components/Shop/ShopLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { AlertCircle } from "lucide-react";
import { motion } from "framer-motion";
import ShopNavbar from "./ShopNavbar";
import LoadingSpinner from "../LoadingSpinner";
import PointsDrawer from "./PointsDrawer";

const PUBLISHABLE_KEY = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY;
const ALLOWED_EMAIL_DOMAIN = "@asu.edu";
Expand Down Expand Up @@ -130,6 +131,7 @@ const ShopLayout: React.FC = () => {
<ClerkProvider publishableKey={PUBLISHABLE_KEY}>
<EmailDomainGuard>
<ShopNavbar />
<PointsDrawer />
<Outlet />
</EmailDomainGuard>
</ClerkProvider>
Expand Down
20 changes: 8 additions & 12 deletions src/pages/PointsSystem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,14 @@ Members can earn points through various activities such as attending meetings, p

## Point System Breakdown

Earn points through different activities to unlock rewards. Here’s a breakdown:

| **Activity Category** | **Specific Activity** | **Points Awarded** | **Notes** |
|---------------------------|----------------------------------------------------------------------------------------|--------------------|---------------------------------------------------------------------------------------------|
| **Meetings** | Attend in-person meeting | 10 | Higher points for in-person attendance |
| | Attend online meeting | Approx. 5 | Encourages participation regardless of location |
| | Attend Lounge hours | 5 | |
| **Active Participation** | Asking questions or contributing to meetings by interacting with workshops/presenters | 1 each time | Promotes lively discussions and engagement |
| | Bonus points for participation | 5 | |
| **Discord Engagement** | Engaging with the Discord in general and asking questions (not answering questions) | 2 per month | Maximum of 10 points per semester; subject to director's discretion |
| | Helpful post or solving a query | 5 per instance | Encourages peer support (cap of 15 points per semester) |
| **Workshop Contribution** | Submit a workshop challenge | 20 | Encourages taking the initiative to engage with the events |
Earn points through different event types. Here’s the updated breakdown:

| **Event Type** | **Value (in Points)** |
| --- | --- |
| GBM | 10 |
| Collaboration (with other clubs) | 15 |
| Hackathon/Bootcamp | 20 |
| Lounge Hour | 15 |
`;

export default function PointsSystem() {
Expand Down
9 changes: 2 additions & 7 deletions src/pages/Shop/ShopIndex.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ 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 @@ -63,13 +62,9 @@ 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 lg:overflow-hidden flex flex-col">
<div className="snap-section relative overflow-y-auto lg:overflow-hidden flex flex-col">
<div className="flex-1 flex flex-col justify-center">
<ProductCarousel
slides={carouselSlides}
autoplayDelay={5000}
rightPanel={<PointsBreakdownTable />}
/>
<ProductCarousel videoUrl="/Soda.mp4" slides={carouselSlides} autoplayDelay={5000} />
</div>

{/* Scroll indicator */}
Expand Down
Loading