diff --git a/client/public/heart.png b/client/public/heart.png new file mode 100644 index 0000000..008f6ef Binary files /dev/null and b/client/public/heart.png differ diff --git a/client/src/components/ui/eventCarousel.tsx b/client/src/components/ui/eventCarousel.tsx new file mode 100644 index 0000000..b700ecb --- /dev/null +++ b/client/src/components/ui/eventCarousel.tsx @@ -0,0 +1,125 @@ +import { ChevronLeft, ChevronRight } from "lucide-react"; +import Image from "next/image"; +import Link from "next/link"; +import { useEffect, useRef, useState } from "react"; + +import { UiEvent as EventType } from "@/hooks/useEvents"; + +type EventCarouselProps = { + items: EventType[]; +}; + +const GAP = 40; + +export default function EventCarousel({ items }: EventCarouselProps) { + const viewportRef = useRef(null); + const firstItemRef = useRef(null); + + const [currentIndex, setCurrentIndex] = useState(0); + const [visibleCount, setVisibleCount] = useState(3); + const [itemWidth, setItemWidth] = useState(0); + + const maxIndex = Math.max(items.length - visibleCount, 0); + const slideLeft = () => { + setCurrentIndex((prev) => Math.max(prev - 1, 0)); + }; + const slideRight = () => { + setCurrentIndex((prev) => Math.min(prev + 1, maxIndex)); + }; + const translateX = -(currentIndex * (itemWidth + GAP)); + + /* Observe item width */ + useEffect(() => { + if (!firstItemRef.current) return; + const observer = new ResizeObserver(() => { + const width = firstItemRef.current?.clientWidth ?? 0; + setItemWidth(width); + }); + observer.observe(firstItemRef.current); + return () => observer.disconnect(); + }, []); + + useEffect(() => { + const updateVisibleCount = () => { + if (window.innerWidth < 768) { + setVisibleCount(1); + } else { + setVisibleCount(3); + } + }; + updateVisibleCount(); + window.addEventListener("resize", updateVisibleCount); + return () => window.removeEventListener("resize", updateVisibleCount); + }, []); + + return ( +
+
+
+

+ Upcoming Events +

+ +
+ + +
+
+ + + See More + +
+ +
+
+
+ {items.map((event, index) => ( +
+
+ {event.name} +
+ +

+ {event.name} +

+ + {/* Needs proper processing and laying out */} +

+ {event.startTime} +

+ +
+
+ ))} +
+
+
+
+ ); +} diff --git a/client/src/components/ui/eventHighlightCard.tsx b/client/src/components/ui/eventHighlightCard.tsx new file mode 100644 index 0000000..76edf72 --- /dev/null +++ b/client/src/components/ui/eventHighlightCard.tsx @@ -0,0 +1,80 @@ +import Image from "next/image"; + +export type eventHighlightCardImage = { + url: string; + width: number; + height: number; + alt: string; +}; + +export type eventHighlightCardType = { + id: number; + title: string; + description: string; + type: string; + image: eventHighlightCardImage | null; + row: number; +}; + +// Purple card header section. +const renderCardHeader = (card: eventHighlightCardType) => { + // Renders differently if we want the techno border. + if (card.type === "special-border") { + return ( +
+
+ {card.title} +
+
+ ); + } + + return ( +
+ {card.title} +
+ ); +}; + +export function EventHighlightCard({ + id, + title, + description, + type, + image, + row, +}: eventHighlightCardType) { + return ( +
+ {renderCardHeader({ id, title, description, type, image, row })} + +
+
+ +

{description}

+ {image && ( + {image.alt} + )} +
+
+
+ ); +} diff --git a/client/src/pages/index.tsx b/client/src/pages/index.tsx index 770d96d..cefdb53 100644 --- a/client/src/pages/index.tsx +++ b/client/src/pages/index.tsx @@ -1,30 +1,22 @@ import Image from "next/image"; import Link from "next/link"; +import EventCarousel from "@/components/ui/eventCarousel"; +import { + EventHighlightCard, + eventHighlightCardType, +} from "@/components/ui/eventHighlightCard"; +import { placeholderEvents, placeholderGames } from "@/placeholderData"; + import { Button } from "../components/ui/button"; export default function Landing() { - const btnList = [ - { name: "More about us", link: "/committee/about", type: "default" }, - { name: "Join our Discord", link: "", type: "outline" }, + const gameLogoImages = [ + { url: "/godot.png", alt: "Godot Logo", position: "start" }, + { url: "/unity-logo.png", alt: "Unity Logo", position: "end" }, ]; - type cardImage = { - url: string; - width: number; - height: number; - alt: string; - }; - - type cardType = { - id: number; - title: string; - description: string; - type: string; - image: cardImage | null; - row: number; - }; - const eventCards = [ + const eventCards: eventHighlightCardType[] = [ { id: 1, title: "Game Jams", @@ -68,66 +60,6 @@ export default function Landing() { }, ]; - const logoImages = [ - { url: "/godot.png", alt: "Godot Logo", position: "start" }, - { url: "/unity-logo.png", alt: "Unity Logo", position: "end" }, - ]; - - const row1Cards = eventCards.filter((card) => card.row === 1); - const row2Cards = eventCards.filter((card) => card.row === 2); - - const renderCardHeader = (card: cardType) => { - if (card.type === "special-border") { - return ( -
-
- {card.title} -
-
- ); - } - - return ( -
- {card.title} -
- ); - }; - - const renderCard = (card: cardType) => ( -
- {renderCardHeader(card)} - -
-
- -

{card.description}

- {card.image && ( - {card.image.alt} - )} -
-
-
- ); - return (
@@ -138,20 +70,15 @@ export default function Landing() {

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut - enim ad minim veniam, quis nostrud exercitation ullamco laboris - nisi ut aliquip ex ea commodo consequat. + eiusmod tempor incididunt ut labore et dolore magna aliqua.

- {btnList.map((item, i) => ( - - - - ))} + + + + + +
@@ -177,27 +104,99 @@ export default function Landing() {
- {row1Cards.map(renderCard)} + {eventCards + .filter((card) => card.row === 1) + .map((card) => ( + + ))}
- {row2Cards.map(renderCard)} + {eventCards + .filter((card) => card.row === 2) + .map((card) => ( + + ))}
- {logoImages.map((logo, index) => ( + {gameLogoImages.map((logo, index) => ( {logo.alt} ))}
+ +
+ +
+ {/* Leaving commented out until styling/design is confirmed. */} + {/*
+
+ +
+
*/} + +
+
+
+
+

+ Featured Member Creations + +

+
+ +
+ + + + + + +
+
+ +
+ {placeholderGames.map((game) => ( +
+
+ {game.name} +
+

+ {game.name} +

+ +

{game.description}

+ +
+
+ ))} +
+
+
); } diff --git a/client/src/placeholderData.ts b/client/src/placeholderData.ts new file mode 100644 index 0000000..0726224 --- /dev/null +++ b/client/src/placeholderData.ts @@ -0,0 +1,83 @@ +export const placeholderEvents = [ + { + id: 1, + name: "Event 1", + time: "Monday 24th Oct 11:00am–4:00pm", + description: "", + publicationDate: "", + date: "", + startTime: "2:00", + location: "", + coverImage: "/landing_placeholder.png", + }, + { + id: 2, + name: "Event 2", + time: "Monday 24th Oct 11:00am–4:00pm", + description: "", + publicationDate: "", + date: "", + startTime: "2:00", + location: "", + coverImage: "/landing_placeholder.png", + }, + { + id: 3, + name: "Event 3", + time: "Monday 24th Oct 11:00am–4:00pm", + description: "", + publicationDate: "", + date: "", + startTime: "2:00", + location: "", + coverImage: "/landing_placeholder.png", + }, + { + id: 4, + name: "Event 4", + time: "Monday 24th Oct 11:00am–4:00pm", + description: "", + publicationDate: "", + date: "", + startTime: "2:00", + location: "", + coverImage: "/landing_placeholder.png", + }, +]; + +// Roughly reflects the game model but may need some finetuning. +export const placeholderGames = [ + { + id: 1, + name: "Game 1", + description: "Game 1 description", + completion: 1, + active: true, + hostURL: "/", + itchEmbedID: "1", + thumbnail: "/landing_placeholder.png", + event: 1, + }, + { + id: 2, + name: "Game 2", + description: "Game 2 description", + completion: 1, + active: true, + hostURL: "/", + itchEmbedID: "1", + thumbnail: "/landing_placeholder.png", + event: 1, + }, + { + id: 3, + name: "Game 3", + description: "Game 3 description", + completion: 1, + active: true, + hostURL: "/", + itchEmbedID: "1", + thumbnail: "/landing_placeholder.png", + event: 1, + }, +];