diff --git a/.gitignore b/.gitignore index 3fb8af8..5628e15 100644 --- a/.gitignore +++ b/.gitignore @@ -38,5 +38,8 @@ next-env.d.ts *storybook.log /.env +public/speakers2 +.env + public/speakers2 .env \ No newline at end of file diff --git a/app/agenda/agenda.tsx b/app/agenda/agenda.tsx index b43a5e1..f0fd668 100644 --- a/app/agenda/agenda.tsx +++ b/app/agenda/agenda.tsx @@ -4,18 +4,40 @@ import * as Select from "@/components/select/Select"; import { Session as SessionComponent } from "@/components/session"; import { Text } from "@/components/text"; import { Toggle } from "@/components/toggle"; -import { Session, Stages, Tracks } from "@/model/session"; +// import { Session, Stages, Tracks } from "@/model/session"; import * as Separator from "@radix-ui/react-separator"; import classNames from "classnames"; import React, { useState } from "react"; +import { + Session, + Stages, + Tracks, + Speaker, +} from "@/components/service/contentStrapi_static"; -type AgendaProps = { sessions: Session[] }; +type AgendaProps = { sessions: Session[]; speakers: Speaker[] }; -export const Agenda: React.FC = ({ sessions }) => { +export const Agenda: React.FC = ({ sessions, speakers }) => { + const [titleFilter, setTitleFilter] = useState(""); const [dayFilter, setDayFilter] = useState(); const [trackFilter, setTrackFilter] = useState(); const [stageFilter, setStageFilter] = useState(); + const stageDisplayNames: Record = { + "Stage 1": "Turing Stage", + "Stage 2": "Hopper Stage", + "Stage 3": "Nakamoto Stage", + "Workshop Room": "Lovelace Room", + }; + + const STAGE_PRIORITY: Record = { + "Stage 3": 0, // Nakamoto — highest priority + "Stage 1": 1, // Turing + "Stage 2": 2, // Hopper + "Workshop Room": 3, // Lovelace + }; + const SAME_TIME_WINDOW_MS = 0 * 60 * 1000; // 5 minutes + function isSameDay(d1: Date, d2: Date) { return ( d1.getFullYear() === d2.getFullYear() && @@ -27,12 +49,41 @@ export const Agenda: React.FC = ({ sessions }) => { let filteredSessions = null; if (sessions) { - filteredSessions = sessions.filter( - (item) => - (!dayFilter || isSameDay(dayFilter, new Date(item.startTime))) && - (trackFilter === "all" || !trackFilter || trackFilter === item.track) && - (stageFilter === "all" || !stageFilter || stageFilter === item.room), - ); + filteredSessions = sessions.filter((item) => { + const matchesDay = + !dayFilter || isSameDay(dayFilter, new Date(item.startTime)); + const matchesTrack = + trackFilter === "all" || !trackFilter || trackFilter === item.track; + const matchesStage = + stageFilter === "all" || !stageFilter || stageFilter === item.room; + const matchesTitle = + !titleFilter.trim() || + item.title.toLowerCase().includes(titleFilter.trim().toLowerCase()); + + return matchesDay && matchesTrack && matchesStage && matchesTitle; + }); + + filteredSessions.sort((a, b) => { + const ta = new Date(a.startTime).getTime(); + const tb = new Date(b.startTime).getTime(); + + if (ta !== tb) { + // If they're close in time, apply stage priority + if (Math.abs(ta - tb) <= SAME_TIME_WINDOW_MS) { + const pa = STAGE_PRIORITY[a.room] ?? 99; + const pb = STAGE_PRIORITY[b.room] ?? 99; + if (pa !== pb) return pa - pb; + } + return ta - tb; + } + + const pa = STAGE_PRIORITY[a.room] ?? 99; + const pb = STAGE_PRIORITY[b.room] ?? 99; + if (pa !== pb) return pa - pb; + + if (a.room !== b.room) return a.room.localeCompare(b.room); + return a.title.localeCompare(b.title); + }); } return ( @@ -44,6 +95,18 @@ export const Agenda: React.FC = ({ sessions }) => { Filter +
+ + Title + + setTitleFilter(e.target.value)} + placeholder="Search agenda titles..." + className="w-full rounded-lg text-white border py-2 px-3 bg-black placeholder-gray-500" + /> +
Days @@ -106,7 +169,7 @@ export const Agenda: React.FC = ({ sessions }) => { Any Stage {Stages.map((stage, index) => ( - {stage} + {stageDisplayNames[stage] || stage} ))} @@ -129,7 +192,9 @@ export const Agenda: React.FC = ({ sessions }) => { /> Any Track - {Tracks.map((track, index) => ( + {Tracks.filter( + (track) => track !== "TUM Blockchain Club", + ).map((track, index) => ( = ({ sessions }) => { {track} @@ -157,32 +222,68 @@ export const Agenda: React.FC = ({ sessions }) => {
- {filteredSessions?.map((item, index) => ( - <> - { - // Add divider when there is they change - index > 0 && - new Date(filteredSessions[index - 1].startTime).getDate() < - new Date(item.startTime).getDate() && ( - - ) - } - - - ))} + {filteredSessions?.map((item, index) => { + // --- Warnings if Strapi data is missing --- + if (!item.title) { + console.warn( + `⚠️ Session at index ${index} is missing a title`, + item, + ); + } + if (!item.startTime || !item.endTime) { + console.warn( + `⚠️ Session "${item.title ?? "?"}" has no start or end time`, + item, + ); + } + if (!item.room) { + console.warn( + `⚠️ Session "${item.title ?? "?"}" has no room assigned`, + item, + ); + } + if (!item.track) { + console.warn( + `⚠️ Session "${item.title ?? "?"}" has no track assigned`, + item, + ); + } + if (!item.speakers || Object.keys(item.speakers).length === 0) { + console.warn( + `⚠️ Session "${item.title ?? "?"}" has no speakers`, + item, + ); + } + // ----------------------------------------- + + return ( + + { + // Divider between days + index > 0 && + new Date(filteredSessions[index - 1].startTime).getDate() < + new Date(item.startTime).getDate() && ( + + ) + } + + + ); + })} + {filteredSessions?.length === 0 && ( There is no session with that filter :( diff --git a/app/agenda/page.tsx b/app/agenda/page.tsx index 1f23f47..bcde515 100644 --- a/app/agenda/page.tsx +++ b/app/agenda/page.tsx @@ -5,125 +5,12 @@ import { Container } from "@/components/container"; import { Text } from "@/components/text"; import { useSession } from "@/hooks/useSession"; import { Session, Stages, Tracks } from "@/model/session"; +import { fetchSessions } from "@/components/service/contentStrapi_static"; +import { fetchSpeakers } from "@/components/service/contentStrapi"; const AgendaPage = async () => { - const sessions: Session[] = [ - { - title: "Blockchain 101: The Historic Evolution of Blockchain", - description: - "Even though blockchains have been around for just a few years, many projects push the boundaries forward. We provide a short overview of the stepping-stone projects and various design choices to consider. Last, we contextualized many previous decades of research and how current systems are built on top of those experiences.", - startTime: "2024-09-12T09:15:00+02:00", - endTime: "2024-09-12T09:25:00+02:00", - room: Stages[1], - track: Tracks[0], - isSpecialSession: true, - type: "Talk", - speakers: [ - { - name: "Filip Rezabek", - description: "PhD, TUM", - priority: 2, - }, - ], - }, - { - title: "New Forms of Money", - description: - "Overview on emerging DLT-based forms of money incl. Tokenized Deposits, Stablecoins and CBDCs.", - startTime: "2024-09-12T11:00:00+02:00", - endTime: "2024-09-12T12:00:00+02:00", - room: Stages[2], - track: Tracks[1], - isSpecialSession: false, - type: "Talk", - speakers: [ - { - name: "Maximilian Baum", - description: "Digital Currencies, Deutsche Bank", - priority: 1, - }, - ], - }, - { - title: "Ethereum Protocol R&D Roadmap", - description: - "The talk explains the latest roadmap of Ethereum protocol development. It provides a technical dive into the current state of the protocol, recent and upcoming upgrades. We will dive into proposed solutions like PeerDAS, EOF, verkle trees and more.", - startTime: "2024-09-12T11:00:00+02:00", - endTime: "2024-09-12T12:00:00+02:00", - room: Stages[3], - track: Tracks[2], - isSpecialSession: false, - type: "Workshop", - speakers: [ - { - name: "Mario Havel", - description: "Protocol Supporter, Ethereum Foundation", - priority: 2, - }, - { - name: "David Kim", - description: "Protocol Architect", - priority: 3, - }, - ], - }, - { - title: "Introduction to Regulation of Crypto Assets", - description: - "Alireza will present an Introduction to the Regulation of Crypto Assets, beginning with an overview of how crypto asset regulation has evolved from both a global and EU perspective. He will also explore the future direction of these regulations. The presentation will delve into the regulation of specific use cases and provide an in-depth discussion on the regulation of decentralized finance (DeFi).", - startTime: "2024-09-13T10:30:00+02:00", - endTime: "2024-09-13T11:30:00+02:00", - room: Stages[3], - track: Tracks[3], - isSpecialSession: false, - type: "Talk", - speakers: [ - { - name: "Alireza Siadat", - description: "Crypto & DLT Advisor", - priority: 2, - }, - ], - }, - { - title: - "Enhancing Smart Contract Security through AI: Promises and Limitations", - description: - "This talk explores the potential of AI in advancing DeFi security. We'll examine how AI can guide fuzzers to uncover smart contract vulnerabilities and work towards real-time detection of exploit transactions. While AI offers exciting possibilities, it's not a silver bullet. We'll balance the discussion by highlighting areas where traditional approaches may still have an edge, providing a comprehensive view of the current state and future potential of AI in DeFi security.", - startTime: "2024-09-13T10:30:00+02:00", - endTime: "2024-09-13T11:30:00+02:00", - room: Stages[0], - track: Tracks[4], - isSpecialSession: true, - type: "Talk", - speakers: [ - { - name: "Arthur Gervais", - description: "Prof. of Information Security, UCL & Co-Founder, D23E", - priority: 2, - }, - ], - }, - { - title: - "Deanonymizing Ethereum Validators: The P2P Network Has a Privacy Issue", - description: - "This presentation reveals how easily validators can be deanonymized in the Ethereum P2P network. We explore extracted data such as validator distribution and geolocation, discuss associated security risks, and propose solutions to improve privacy.", - startTime: "2024-09-13T10:30:00+02:00", - endTime: "2024-09-13T11:30:00+02:00", - room: Stages[3], - track: Tracks[5], - isSpecialSession: true, - type: "Talk", - speakers: [ - { - name: "Yann Vonlanthen", - description: "PhD student, ETH Zurich", - priority: 2, - }, - ], - }, - ]; + const sessions = await fetchSessions(); + const speakers = await fetchSpeakers(); return (
@@ -139,7 +26,7 @@ const AgendaPage = async () => {
- +
diff --git a/components/header/Header.tsx b/components/header/Header.tsx index 2cccc8a..8157d82 100644 --- a/components/header/Header.tsx +++ b/components/header/Header.tsx @@ -33,7 +33,7 @@ const links: HeaderLink[] = [ showsAtHome: true, }, { label: "Side Events", link: "/side-events", showsAtHome: true }, - // { label: "Agenda", link: "/agenda", showsAtHome: true }, + { label: "Agenda", link: "/agenda", showsAtHome: true }, // { label: "Workshops", link: "/workshops", showsAtHome: true }, // { label: "Student Grants", link: "#grants", showsAtHome: true }, // { label: "FAQ", link: "#faq", showsAtHome: true }, diff --git a/components/service/contentStrapi_static.ts b/components/service/contentStrapi_static.ts new file mode 100644 index 0000000..383bb1c --- /dev/null +++ b/components/service/contentStrapi_static.ts @@ -0,0 +1,129 @@ +import axios from "axios"; + +export interface ImageFormat { + name: string; + hash: string; + ext: string; + mime: string; + path: string | null; + width: number; + height: number; + size: number; + sizeInBytes: number; + url: string; +} + +export interface ProfilePicture { + id: number; + documentId: string; + name: string; + alternativeText: string | null; + caption: string | null; + width: number; + height: number; + formats: { + thumbnail: ImageFormat; + large: ImageFormat; + medium: ImageFormat; + small: ImageFormat; + [key: string]: ImageFormat; + }; + hash: string; + ext: string; + mime: string; + size: number; + url: string; + previewUrl: string | null; + provider: string; + provider_metadata: never | null; + createdat: string; + updatedat: string; + publishedat: string; +} + +export interface Speaker { + id: number; + documentId: string; + name: string; + company_name: string; + url: string; + priority: number; + createdAt: string; + updatedAt: string; + publishedAt: string; + position: string; + profile_photo?: ProfilePicture | null; +} + +export const Tracks = [ + "Application", + "Ecosystem", + "Education", + "Research", + "Regulation", + "Workshop", + "TUM Blockchain Club", + // "Sub Events", + // "TUM Blockchain Club", +] as const; + +export const Stages = [ + "Stage 1", + "Stage 2", + "Stage 3", + "Workshop Room", +] as const; + +export interface Session { + id: number; + documentId: string; + title: string; + track?: (typeof Tracks)[number] | null; + type?: "Workshop" | "Panel Discussion" | "Talk" | null; + startTime: string; + endTime: string; + room: (typeof Stages)[number]; + description?: string | null; + speakers?: Record | null; + isSpecialSession?: boolean | null; + registrationLink?: string | null; + createdAt: string; + updatedAt: string; + publishedAt: string; +} + +export const fetchSessions = async (): Promise => { + const token = process.env.STRAPI_API_TOKEN; + if (!token) { + console.warn("STRAPI_API_TOKEN missing; returning empty sessions list"); + return []; + } + + try { + const sessions: Session[] = []; + let hasMore = true; + let page = 1; + + do { + const res = await axios.get( + `https://strapi.rbg.tum-blockchain.com/api/agenda-25s?sort=startTime:asc&pagination[page]=${page}&pagination[pageSize]=25`, + { + headers: { Authorization: `Bearer ${token}` }, + }, + ); + + const pageData: Session[] = res.data.data; + sessions.push(...pageData); + + hasMore = + res.data.meta.pagination.page < res.data.meta.pagination.pageCount; + page++; + console.log(`Fetched ${sessions.length} sessions so far...`); + } while (hasMore); + + return sessions; + } catch (err) { + console.error("Error fetching sessions from Strapi:", err); + return []; + } +}; diff --git a/components/session/Session.tsx b/components/session/Session.tsx index ec7e3c2..df44062 100644 --- a/components/session/Session.tsx +++ b/components/session/Session.tsx @@ -2,7 +2,10 @@ import { Button } from "@/components/button"; import { Text } from "@/components/text"; -import { Session as SessionModel } from "@/model/session"; +import { + Session as SessionModel, + Speaker, +} from "@/components/service/contentStrapi_static"; import { ClockIcon, SewingPinIcon } from "@radix-ui/react-icons"; import classNames from "classnames"; @@ -10,19 +13,21 @@ import Image from "next/image"; import Link from "next/link"; import React, { useEffect, useRef, useState } from "react"; import { contentfulImageLoader } from "@/util/contentfulImageLoader"; -import { Clock, MapPin, Bell } from "lucide-react"; +import { Clock, MapPin } from "lucide-react"; export type SessionElement = React.ElementRef<"div">; export type SessionProps = React.ComponentPropsWithoutRef<"div"> & { session: SessionModel; + speakers: Speaker[]; }; export const Session = React.forwardRef( (props, ref) => { - const { session, className, ...divProps } = props; + const { session, speakers, className, ...divProps } = props; const [clamped, setClamped] = useState(true); const [isLineClampClamped, setIsLineClampClamped] = useState(false); const lineClampRef = useRef(null); + const [active, setActive] = useState(false); useEffect(() => { const checkLineClamping = () => { @@ -49,6 +54,10 @@ export const Session = React.forwardRef( endTime: new Date(session.endTime), }; + const speakerMap = new Map( + speakers.map((sp) => [sp.name.toLowerCase().trim(), sp]), + ); + return (
( className, "border w-full flex p-4 flex-col gap-4 bg-gradient-to-b from-black bg-opacity-60", { - "to-[#14532d]/60": session.track === "Education Track", // Dark forest green - "to-[#665200]/60": session.track === "Research Track", // Deep gold-brown - "to-[#1e3a8a]/40": session.track === "Ecosystem Track", // Deep blue (Tailwind blue-900) - "to-[#4c0608]/60": session.track === "Regulation Track", // Deep red / oxblood - "to-[#1a012e]": session.track === "Academic Track", // Very dark purple - "to-[#134e4a]/60": session.track === "Application Track", // Teal-950 (deep cyan-green) + "to-[#14532d]/60": session.track === "Education", // Dark forest green + "to-[#665200]/60": session.track === "Research", // Deep gold-brown + "to-[#1e3a8a]/40": session.track === "Ecosystem", // Deep blue (Tailwind blue-900) + "to-[#4c0608]/60": session.track === "Regulation", // Deep red / oxblood + "to-[#1a012e]": session.track === "Workshop", // Very dark purple + "to-[#134e4a]/60": session.track === "Application", // Teal-950 (deep cyan-green) }, )} ref={ref} @@ -102,28 +111,30 @@ export const Session = React.forwardRef( className={classNames( "border rounded-[5px] h-fit col-start-2", { - "border-green-400": session.track === "Education Track", - "border-yellow-400": session.track === "Research Track", - "border-blue-400": session.track === "Ecosystem Track", - "border-amber": session.track === "Research Track", - "border-[#F87171]": session.track === "Regulation Track", - "border-[#c084fc]": session.track === "Academic Track", - "border-teal-400": session.track === "Application Track", + "border-green-400": session.track === "Education", + "border-yellow-400": session.track === "Research", + "border-blue-400": session.track === "Ecosystem", + "border-amber": session.track === "Research", + "border-[#F87171]": session.track === "Regulation", + "border-[#c084fc]": session.track === "Workshop", + "border-teal-400": session.track === "Application", }, )} > - {session.track} + {session.track === "TUM Blockchain Club" + ? "TBC" + : session.track}
)} @@ -132,7 +143,17 @@ export const Session = React.forwardRef(
- {session.room} + + {session.room === "Stage 1" + ? "Turing Stage" + : session.room === "Stage 2" + ? "Hopper Stage" + : session.room === "Stage 3" + ? "Nakamoto Stage" + : session.room === "Workshop Room" + ? "Lovelace Room" + : session.room} +
@@ -157,7 +178,28 @@ export const Session = React.forwardRef(
- + {/* setActive(!active)} + > + {active && ( + + )} + + + */}
@@ -175,14 +217,14 @@ export const Session = React.forwardRef( setClamped(!clamped)} className={classNames("cursor-pointer", { - "text-green-400": session.track === "Education Track", + "text-green-400": session.track === "Education", "text-yellow-400": - session.track === "Research Track" || !session.track, - "text-blue-400": session.track === "Ecosystem Track", - "text-orange-400": session.track === "Research Track", - "text-red-400": session.track === "Regulation Track", - "text-[#E9D5FF]": session.track === "Academic Track", - "text-teal-400": session.track === "Application Track", + session.track === "Research" || !session.track, + "text-blue-400": session.track === "Ecosystem", + "text-orange-400": session.track === "Research", + "text-red-400": session.track === "Regulation", + "text-[#E9D5FF]": session.track === "Workshop", + "text-teal-400": session.track === "Application", })} > {clamped ? "Show More" : "Show Less"} @@ -204,38 +246,52 @@ export const Session = React.forwardRef(
- Speaker{session.speakers && session.speakers.length > 1 && "s"}: -
-
+ Speaker {session.speakers && - session.speakers.map((speaker, index) => ( - <> -
- {speaker.profilePhoto && ( - + Object.keys(session.speakers).length > 1 && + "s"} + : +
+
+ {session.speakers && Object.keys(session.speakers).length > 0 ? ( + Object.values(session.speakers).map((name, index) => { + const details = speakerMap.get(name.toLowerCase().trim()); + + return ( +
+ {/* Show profile photo if found */} + {details?.profile_photo && ( + {speaker.name} )} -
- {speaker.name} - - {speaker.description} +
+ + {details?.name || name} + {details?.position && ( + + {details.position} + {details.company_name + ? `, ${details.company_name}` + : ""} + + )}
- - ))} - {(!session.speakers || session.speakers.length === 0) && ( + ); + }) + ) : ( Coming soon... )}
diff --git a/package-lock.json b/package-lock.json index 6a63b14..c5483a6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "axios": "^1.9.0", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", + "fs": "^0.0.1-security", "jsqr": "^1.4.0", "lucide-react": "^0.477.0", "next": "^14.2.5", @@ -40,6 +41,7 @@ "@storybook/nextjs": "^8.0.8", "@storybook/react": "^8.0.8", "@storybook/test": "^8.0.8", + "@tailwindcss/line-clamp": "^0.4.4", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", @@ -7309,6 +7311,15 @@ "tslib": "^2.4.0" } }, + "node_modules/@tailwindcss/line-clamp": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@tailwindcss/line-clamp/-/line-clamp-0.4.4.tgz", + "integrity": "sha512-5U6SY5z8N42VtrCrKlsTAA35gy2VSyYtHWCsg1H87NU1SXnEfekTVlrga9fzUDrrHcGi2Lb5KenUWb4lRQT5/g==", + "dev": true, + "peerDependencies": { + "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" + } + }, "node_modules/@testing-library/dom": { "version": "9.3.4", "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-9.3.4.tgz", @@ -12936,6 +12947,11 @@ "node": ">= 0.6" } }, + "node_modules/fs": { + "version": "0.0.1-security", + "resolved": "https://registry.npmjs.org/fs/-/fs-0.0.1-security.tgz", + "integrity": "sha512-3XY9e1pP0CVEUCdj5BmfIZxRBTSDycnbqhIOGec9QYtmVH2fbLpj86CFWkrNOkt/Fvty4KZG5lTglL9j/gJ87w==" + }, "node_modules/fs-constants": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", diff --git a/package.json b/package.json index e8681ac..b075215 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "axios": "^1.9.0", "class-variance-authority": "^0.7.0", "classnames": "^2.5.1", + "fs": "^0.0.1-security", "jsqr": "^1.4.0", "lucide-react": "^0.477.0", "next": "^14.2.5",