diff --git a/.gitignore b/.gitignore index 694c8bf36..fca12c4bb 100644 --- a/.gitignore +++ b/.gitignore @@ -128,6 +128,7 @@ dmypy.json .pyre/ .vscode/ +.idea/ out/ .next/ .DS_Store @@ -138,3 +139,11 @@ storybook-static/ # Sentry Auth Token .sentryclirc .astro + +# EuroPython website +src/content/speakers +src/content/sessions +src/content/days +src/data/speakers.json +src/data/sessions.json +src/data/schedule.json diff --git a/Makefile b/Makefile index 381952333..e434ba371 100644 --- a/Makefile +++ b/Makefile @@ -19,10 +19,10 @@ BRANCH ?= $(shell git rev-parse --abbrev-ref HEAD) # Replace "/" and other non-alphanumeric characters with "-" SAFE_BRANCH := $(shell echo "$(BRANCH)" | sed 's/[^A-Za-z0-9-]/-/g') FORCE_DEPLOY ?= false +SITE_URL ?= "https://$(SAFE_BRANCH).ep-preview.click" .PHONY: build deploy dev clean install - safe_branch: @echo $(SAFE_BRANCH) @@ -47,6 +47,7 @@ build: preview: RELEASES_DIR = $(VPS_PREVIEW_PATH)/$(SAFE_BRANCH)/releases preview: TARGET = $(RELEASES_DIR)/$(TIMESTAMP) preview: + @echo "Preview site URL: $(SITE_URL)" # Output preview URL echo $(TARGET) @echo "\n\n**** Deploying preview of a branch '$(BRANCH)' (safe: $(SAFE_BRANCH)) to $(TARGET)...\n\n" $(REMOTE_CMD) "mkdir -p $(TARGET)" diff --git a/astro.config.mjs b/astro.config.mjs index bdd7f84d3..48908689c 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,4 +1,5 @@ -import path from "path"; +import path, { dirname } from "path"; +import { fileURLToPath } from "url"; import { defineConfig } from "astro/config"; import mdx from "@astrojs/mdx"; import sitemap from "@astrojs/sitemap"; @@ -59,7 +60,6 @@ export default defineConfig({ "/sponsor/": "/sponsorship/sponsor/", "/voting/": "/programme/voting/", "/wasm-summit/": "/programme/wasm-summit/", - "/sessions/": "/programme/sessions/", }, integrations: [ mdx(), @@ -76,7 +76,11 @@ export default defineConfig({ build: { minify: true, }, + image: { + experimentalLayout: "responsive", + }, experimental: { + responsiveImages: true, svg: true, }, }); diff --git a/package.json b/package.json index 443d0c0ac..2873865d7 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,8 @@ "date-fns": "^4.1.0", "date-fns-tz": "^3.2.0", "hastscript": "^9.0.0", + "js-yaml": "^4.1.0", + "marked": "^15.0.7", "pagefind": "^1.3.0", "react": "^19.1.0", "react-dom": "^19.1.0", @@ -40,6 +42,7 @@ "typescript": "^5.8.3" }, "devDependencies": { + "@types/js-yaml": "^4.0.9", "prettier": "^3.4.2", "prettier-plugin-astro": "^0.14.1" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b0594a015..f02e109e4 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -62,6 +62,12 @@ importers: hastscript: specifier: ^9.0.0 version: 9.0.1 + js-yaml: + specifier: ^4.1.0 + version: 4.1.0 + marked: + specifier: ^15.0.7 + version: 15.0.7 pagefind: specifier: ^1.3.0 version: 1.3.0 @@ -90,6 +96,9 @@ importers: specifier: ^5.8.3 version: 5.8.3 devDependencies: + '@types/js-yaml': + specifier: ^4.0.9 + version: 4.0.9 prettier: specifier: ^3.4.2 version: 3.5.3 @@ -1160,6 +1169,9 @@ packages: '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/js-yaml@4.0.9': + resolution: {integrity: sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==} + '@types/mdast@4.0.4': resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} @@ -1877,6 +1889,11 @@ packages: markdown-table@3.0.4: resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + marked@15.0.7: + resolution: {integrity: sha512-dgLIeKGLx5FwziAnsk4ONoGwHwGPJzselimvlVskE9XLN4Orv9u2VA3GWw/lYUqjfA0rUT/6fqKwfZJapP9BEg==} + engines: {node: '>= 18'} + hasBin: true + mdast-util-definitions@6.0.0: resolution: {integrity: sha512-scTllyX6pnYNZH/AIp/0ePz6s4cZtARxImwoPJ7kS42n+MnVsI4XbnG6d4ibehRIldYMWM2LD7ImQblVhUejVQ==} @@ -3958,6 +3975,8 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/js-yaml@4.0.9': {} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 @@ -4854,6 +4873,8 @@ snapshots: markdown-table@3.0.4: {} + marked@15.0.7: {} + mdast-util-definitions@6.0.0: dependencies: '@types/mdast': 4.0.4 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 000000000..8aed2cd00 Binary files /dev/null and b/public/favicon.ico differ diff --git a/scripts/download-data.py b/scripts/download-data.py deleted file mode 100644 index 88a27bd2f..000000000 --- a/scripts/download-data.py +++ /dev/null @@ -1,79 +0,0 @@ -# /// script -# dependencies = [ -# "httpx", -# "PyYAML", -# ] -# requires-python = ">=3.11" -# /// - -from typing import Any -import httpx -import json -import pathlib -import shutil -import yaml - -ROOT = pathlib.Path(__file__).parents[1] - - -SESSIONS_URL = "https://programapi24.europython.eu/2024/sessions.json" -SPEAKERS_URL = "https://programapi24.europython.eu/2024/speakers.json" -SCHEDULE_DATA = "https://programapi24.europython.eu/2024/schedule.json" - - -def write_mdx(data: dict[str, Any], output_dir: pathlib.Path, content_key: str) -> None: - if output_dir.exists(): - shutil.rmtree(output_dir) - - output_dir.mkdir(parents=True, exist_ok=True) - - for key, value in data.items(): - filename = f"{key}.mdx" - path = output_dir / filename - - content = value.pop(content_key) or "" - - content = content.replace("<3", "❤️") - - frontmatter = yaml.dump(value, sort_keys=True) - - with path.open("w", encoding="utf-8") as f: - f.write(f"---\n{frontmatter}---\n\n{content}") - - -def download_data(url: str) -> dict[str, Any]: - with httpx.Client() as client: - response = client.get(url) - response.raise_for_status() - data = response.json() - - return data - - -def download() -> None: - speakers = download_data(SPEAKERS_URL) - sessions = download_data(SESSIONS_URL) - schedule = download_data(SCHEDULE_DATA) - - for session in sessions.values(): - session["speakers"] = [ - speakers[speaker_id]["slug"] for speaker_id in session.get("speakers", []) - ] - - for speaker in speakers.values(): - speaker["submissions"] = [ - sessions[session_id]["slug"] - for session_id in speaker.get("submissions", []) - if session_id in sessions - ] - - write_mdx(sessions, ROOT / "src/content/sessions", "abstract") - write_mdx(speakers, ROOT / "src/content/speakers", "biography") - - for day, data in schedule["days"].items(): - path = ROOT / f"src/content/days/{day}.json" - with path.open("w", encoding="utf-8") as f: - json.dump(data, f, indent=2) - - -download() diff --git a/src/components/schedule/speakers.astro b/src/components/schedule/speakers.astro index a69b4015f..33e851e80 100644 --- a/src/components/schedule/speakers.astro +++ b/src/components/schedule/speakers.astro @@ -20,7 +20,7 @@ const speakers = Astro.props.speakers { speakers.map((speaker, index) => ( - + {speaker.data.name} {index < speakers.length - 1 ? ", " : ""} diff --git a/src/components/session-speakers.astro b/src/components/session-speakers.astro index 4baa0fbad..fc2be7e80 100644 --- a/src/components/session-speakers.astro +++ b/src/components/session-speakers.astro @@ -21,7 +21,7 @@ const speakers = await getEntries( { speakers.map((speaker, index) => ( - + {speaker.data.name} {index < speakers.length - 1 ? ", " : ""} diff --git a/src/components/ui/Headline.astro b/src/components/ui/Headline.astro index fae8a6d54..ef3ffeabe 100644 --- a/src/components/ui/Headline.astro +++ b/src/components/ui/Headline.astro @@ -6,7 +6,7 @@ const isAnchor = !!id; const isLink = !!href; --- - + {isAnchor && ( {Title} @@ -14,3 +14,29 @@ const isLink = !!href; )} {isLink && {Title} } + + diff --git a/src/components/ui/Markdown.astro b/src/components/ui/Markdown.astro new file mode 100644 index 000000000..ca465b4a9 --- /dev/null +++ b/src/components/ui/Markdown.astro @@ -0,0 +1,14 @@ +--- +import { marked } from 'marked'; + +interface Props { + content: string; +} + +const { content } = Astro.props; +const html = marked.parse(content); +--- + +
+
+
diff --git a/src/content/config.ts b/src/content/config.ts index 33e828647..6b70ace68 100644 --- a/src/content/config.ts +++ b/src/content/config.ts @@ -1,4 +1,5 @@ import { defineCollection, reference, z } from "astro:content"; +import { file } from "astro/loaders"; const tiers = [ "Keystone", @@ -54,12 +55,70 @@ const keynoters = defineCollection({ }), }); +// Cache for fetched data to prevent duplicate network requests +let cachedSpeakersData: any = null; +let cachedSessionsData: any = null; + +// Shared data fetching function +async function getCollectionsData() { + // Only fetch if not already cached + if (!cachedSpeakersData || !cachedSessionsData) { + const [speakersResponse, sessionsResponse] = await Promise.all([ + fetch( + "https://gist.github.com/egeakman/469f9abb23a787df16d8787f438dfdb6/raw/62d2b7e77c1b078a0e27578c72598a505f9fafbf/speakers.json" + ), + fetch( + "https://gist.githubusercontent.com/egeakman/eddfb15f32ae805e8cfb4c5856ae304b/raw/466f8c20c17a9f6c5875f973acaec60e4e4d0fae/sessions.json" + ), + ]); + + cachedSpeakersData = await speakersResponse.json(); + cachedSessionsData = await sessionsResponse.json(); + } + + // Create indexed versions for efficient lookups + const speakersById = Object.entries(cachedSpeakersData).reduce( + (acc, [id, speaker]: [string, any]) => { + acc[id] = { id, ...speaker }; + return acc; + }, + {} as Record + ); + + const sessionsById = Object.entries(cachedSessionsData).reduce( + (acc, [id, session]: [string, any]) => { + acc[id] = { id, ...session }; + return acc; + }, + {} as Record + ); + + return { + speakersData: cachedSpeakersData, + sessionsData: cachedSessionsData, + speakersById, + sessionsById, + }; +} + const speakers = defineCollection({ - type: "content", + loader: async (): Promise => { + const { speakersData, sessionsById } = await getCollectionsData(); + + return Object.values(speakersData).map((speaker: any) => ({ + id: speaker.slug, + ...speaker, + submissions: (speaker.submissions || []) + .filter((sessionId: string) => sessionId in sessionsById) + .map((sessionId: string) => sessionsById[sessionId].slug), + })); + }, schema: z.object({ code: z.string(), name: z.string(), + slug: z.string(), avatar: z.string(), + biography: z.string().nullable(), submissions: z.array(reference("sessions")), affiliation: z.string().nullable(), homepage: z.string().nullable(), @@ -71,10 +130,22 @@ const speakers = defineCollection({ }); const sessions = defineCollection({ - type: "content", + loader: async (): Promise => { + const { sessionsData, speakersById } = await getCollectionsData(); + + return Object.values(sessionsData).map((session: any) => ({ + id: session.slug, + ...session, + speakers: (session.speakers || []) + .filter((speakerId: string) => speakerId in speakersById) + .map((speakerId: string) => speakersById[speakerId].slug), + })); + }, schema: z.object({ code: z.string(), title: z.string(), + slug: z.string(), + abstract: z.string().nullable(), speakers: z.array(reference("speakers")), session_type: z.string(), track: z.string().nullable(), @@ -85,7 +156,7 @@ const sessions = defineCollection({ .nullable(), duration: z.string(), level: z.enum(["beginner", "intermediate", "advanced"]), - delivery: z.enum(["in-person", "remote"]), + delivery: z.enum(["in-person", "remote", ""]), room: z.string().nullable(), start: z.string().nullable(), end: z.string().nullable(), diff --git a/src/content/pages/explore.mdx b/src/content/pages/explore.mdx index a1cfa0587..e0ccc10e8 100644 --- a/src/content/pages/explore.mdx +++ b/src/content/pages/explore.mdx @@ -3,7 +3,6 @@ title: More Tips on Exploring Prague subtitle: At EuroPython, many attendees will come with their families. While not attending the conference, why not go and explore Prague and the surrounding area? --- -import { Image } from "astro:assets"; import venueImage from "./images/prague.jpg"; # Prague Exploration Tips diff --git a/src/content/pages/programme/overview.mdx b/src/content/pages/programme/overview.mdx new file mode 100644 index 000000000..0d54b5fe9 --- /dev/null +++ b/src/content/pages/programme/overview.mdx @@ -0,0 +1,49 @@ +--- +title: Programme Overview +subtitle: Overview of all programme +--- + +The conference will be organised into three phases: + +1. **Monday & Tuesday (14 & 15 July):** Tutorial Days +2. **Wednesday – Friday (16 – 18 July):** Main Conference Days +3. **Saturday & Sunday (19 & 20 July):** Sprint Days + +Expect around: +* 120 talks +* 20 tutorials +* 5–6 keynotes +* open spaces +* 3 summits +* multiple events + +--- + +# Sessions + +## Talks +Talks are 30–45 minute presentations on Python-related topics, held across 6 parallel tracks during the Main Conference Days. + +## Tutorials +Tutorials are 180-minute hands-on sessions focused on learning by doing, held during the Tutorial Days. + +### List of sessions +Can be found on the [sessions page](/sessions). + +# Summits +Summits are full-day, topic-focused unconference-style gatherings for in-depth discussions, held during the Tutorial Days. + +### List of summits +- [Rust Summit](/programme/rust-summit) +- [WASM Summit](/programme/wasm-summit) +- [C API Summit](/programme/c-api-summit) + +# Events +Events are social and community activities held throughout the conference to connect attendees. + +List of events: +- main social event: will be added soon +- speakers dinner: will be added soon +- PyLadies events: will be added soon +- beginner's day unconference space: will be added soon +- workshops for less represented groups in computing: will be added soon diff --git a/src/content/sessions/.gitkeep b/src/content/sessions/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/content/speakers/.gitkeep b/src/content/speakers/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/data/links.json b/src/data/links.json index 3701459a8..7e159ab73 100644 --- a/src/data/links.json +++ b/src/data/links.json @@ -3,9 +3,13 @@ { "name": "Programme", "items": [ + { + "name": "Overview", + "path": "/overview" + }, { "name": "Sessions Previews", - "path": "/programme/sessions" + "path": "/sessions" }, { "name": "Tracks", @@ -19,6 +23,10 @@ "name": "Community Voting", "path": "/programme/voting" }, + { + "name": "List of Speakers", + "path": "/speakers" + }, { "name": "Speaker Mentorship", "path": "/programme/mentorship" diff --git a/src/pages/session/[slug].astro b/src/pages/session/[slug].astro index a04265334..bcfaacb9a 100644 --- a/src/pages/session/[slug].astro +++ b/src/pages/session/[slug].astro @@ -5,11 +5,13 @@ import Prose from "../../components/prose/prose.astro"; import { Separator } from "../../components/separator/separator"; import { formatInTimeZone } from "date-fns-tz"; import { YouTube } from "@astro-community/astro-embed-youtube"; +import { Picture } from "astro:assets"; +import Markdown from "@ui/Markdown.astro"; export async function getStaticPaths() { const sessions = await getCollection("sessions"); return sessions.map((entry) => ({ - params: { slug: entry.slug }, + params: { slug: entry.id}, props: { entry }, })); } @@ -18,15 +20,9 @@ export async function getStaticPaths() { const sessions = await getCollection("sessions"); const { entry } = Astro.props; -const { Content } = await entry.render(); const speakers = await getEntries(entry.data.speakers); -for (const speaker of speakers) { - // @ts-ignore - speaker.Content = (await speaker.render()).Content; -} - // Resolve session codes to session data const resolveSessions = (codes: string[]) => codes.map((code) => sessions.find((s) => s?.data?.code === code)); @@ -106,7 +102,7 @@ const nextSessionsOrdered = sameRoomNextSession

Abstract

- +
{ @@ -155,11 +151,14 @@ const nextSessionsOrdered = sameRoomNextSession
{speaker.data.avatar ? (
- {speaker.data.name} +
+ +
) : ( @@ -201,7 +199,7 @@ const nextSessionsOrdered = sameRoomNextSession {parallelSessions.map((session: any) => (
  • - + {session.data.title} @@ -219,7 +217,7 @@ const nextSessionsOrdered = sameRoomNextSession {nextSessionsOrdered.map((session: any) => (
  • {session.data.title} diff --git a/src/pages/sessions.astro b/src/pages/sessions.astro new file mode 100644 index 000000000..4b1b477d2 --- /dev/null +++ b/src/pages/sessions.astro @@ -0,0 +1,101 @@ +--- +import { getCollection } from "astro:content"; +import Layout from "../layouts/Layout.astro"; +import Prose from "../components/prose/prose.astro"; +import { Separator } from "../components/separator/separator"; + +// Fetch all speaker entries +const sessionsCollection = await getCollection("sessions"); + +// Define the type for the groups object +type Session = { + id: string; + data: { + title: string; + session_type: string; + }; +}; + +type Groups = { + [key: string]: Session[]; +}; + +const groups: Groups = sessionsCollection + .filter((session: Session) => !!session.data.session_type) + .reduce((acc: Groups, session: Session) => { + const sessionType = session.data.session_type; + if (!acc[sessionType]) { + acc[sessionType] = []; + } + acc[sessionType].push(session); + return acc; + }, {} as Groups); + +// Sort session types alphabetically +const sessionTypes = Object.keys(groups).sort((a, b) => a.localeCompare(b)); + +const title = "Sessions"; + +const description = + "List of all confirmed sessions for the conference, sorted by session type."; +--- + + +
    + +

    Sessions

    +

    + Check out all the sessions at the conference. You can filter them by track, type, and level. +

    + + +

    Go to session type:

    +
    + +
      + { + sessionTypes.map((sessionType, index) => ( + <> +
      +

      + {sessionType} +

      + +
        + {groups[sessionType] + .sort((a, b) => a.data.title.localeCompare(b.data.title)) + .map((session) => ( +
      • + + {session.data.title} + +
      • + ))} +
      +
      + + {index !== sessionTypes.length - 1 ? ( + + ) : ( +
      + )} + + )) + } +
    + +
    + diff --git a/src/pages/speaker/[slug].astro b/src/pages/speaker/[slug].astro index cd4dea0d3..580a33f81 100644 --- a/src/pages/speaker/[slug].astro +++ b/src/pages/speaker/[slug].astro @@ -2,17 +2,21 @@ import { getCollection, getEntries } from "astro:content"; import Layout from "../../layouts/Layout.astro"; import Prose from "../../components/prose/prose.astro"; +import { Picture } from "astro:assets"; +import { getEntry, render } from 'astro:content'; +import Markdown from "@ui/Markdown.astro"; +import Headline from "@ui/Headline.astro"; export async function getStaticPaths() { const entries = await getCollection("speakers"); return entries.map((entry) => ({ - params: { slug: entry.slug }, + params: { slug: entry.id}, props: { entry }, })); } -const { entry } = Astro.props; -const { Content } = await entry.render(); +const {entry} = Astro.props; +console.log(entry.data.avatar); const sessions = await getEntries(entry.data.submissions); @@ -50,46 +54,47 @@ function getGitHosting(url: string): string | undefined { function isUrl(str: string): boolean { str = ensureHttps(str); const regex = - /^(https?:\/\/)?([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,6})([\/\w@.-]*)*(\?.*)?$/; + /^(https?:\/\/)?([a-zA-Z0-9.-]+)\.([a-zA-Z]{2,6})([\/\w@.-]*)$/; return regex.test(str); } function ensureHttps(str: string): string { if (!/^https?:\/\//i.test(str)) { - return `https://${str}`; + return 'https://' + str; } return str; } + --- -
    -

    - {entry.data.name} -

    +
    + { - entry.data.avatar && ( -
    - +
    ) } + { - entry.body ? ( + entry.data.biography ? ( <>

    Biography

    - + ) : null @@ -211,7 +216,7 @@ function ensureHttps(str: string): string {
  • {session.data.title} diff --git a/src/pages/speakers.astro b/src/pages/speakers.astro new file mode 100644 index 000000000..f72dc19e6 --- /dev/null +++ b/src/pages/speakers.astro @@ -0,0 +1,93 @@ +--- +import { getCollection } from "astro:content"; +import Layout from "../layouts/Layout.astro"; +import Prose from "../components/prose/prose.astro"; +import { Separator } from "../components/separator/separator"; + +// Fetch all speaker entries +const speakersCollection = await getCollection("speakers"); + +// Define the type for the groups object +type Speaker = { + id: string; + data: { + name: string; + }; +}; + +type Groups = { + [key: string]: Speaker[]; +}; + +// Group speakers by the first letter of their name +const groups: Groups = speakersCollection + .filter((speaker: Speaker) => !!speaker.data.name) + .reduce((acc: Groups, speaker: Speaker) => { + const letter = speaker.data.name[0].toUpperCase(); + if (!acc[letter]) { + acc[letter] = []; + } + acc[letter].push(speaker); + return acc; + }, {} as Groups); + +const letters = Object.keys(groups).sort((a, b) => a.localeCompare(b)); + +const title = "Speakers"; + +const description = + "Alphabetical list of all confirmed speakers for the conference"; +--- + + +
    + +

    Speakers

    +
    + +
    + { + letters.map((letter) => ( +

    + {letter} +

    + )) + } +
    + +
      + { + letters.map((letter, index) => ( + <> +
      +

      + {letter} +

      + +
        + {groups[letter] + .sort((a, b) => a.data.name.localeCompare(b.data.name)) + .map((speaker) => ( +
      • + + {speaker.data.name} + +
      • + ))} +
      +
      + + {index !== letters.length - 1 ? ( + + ) : ( +
      + )} + + )) + } +
    +
    +
    diff --git a/tsconfig.json b/tsconfig.json index 6046422f2..5ef51ac37 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,6 +2,7 @@ "extends": "astro/tsconfigs/strict", "compilerOptions": { "strictNullChecks": true, + "noImplicitAny": false, "jsx": "react-jsx", "jsxImportSource": "react", "paths": {