From 96fd9660a94306d130054e24f93df3f3569770da Mon Sep 17 00:00:00 2001 From: Youssouf EL Azizi Date: Fri, 8 Nov 2024 16:27:18 +0100 Subject: [PATCH] feat: add planing page --- package.json | 1 + pnpm-lock.yaml | 53 +++++ src/components/planning/episode-card.astro | 118 ++++++++++ src/components/planning/index.astro | 75 +++++++ src/lib/notion/index.ts | 120 ++++++++++ src/lib/notion/notion-mock.json | 244 +++++++++++++++++++++ src/lib/notion/types.ts | 196 +++++++++++++++++ src/pages/planning.astro | 10 + src/pages/podcast/[...slug].astro | 7 + 9 files changed, 824 insertions(+) create mode 100644 src/components/planning/episode-card.astro create mode 100644 src/components/planning/index.astro create mode 100644 src/lib/notion/index.ts create mode 100644 src/lib/notion/notion-mock.json create mode 100644 src/lib/notion/types.ts create mode 100644 src/pages/planning.astro diff --git a/package.json b/package.json index c7fb56bb..8d1dcfc3 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@astrojs/rss": "^4.0.7", "@astrolib/seo": "1.0.0-beta.8", "@astropub/md": "^1.0.0", + "@notionhq/client": "^2.2.15", "@radix-ui/react-slot": "^1.1.0", "@resvg/resvg-js": "^2.6.2", "@rive-app/canvas": "^2.21.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index cacc66cb..491ee388 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: '@astropub/md': specifier: ^1.0.0 version: 1.0.0(@astrojs/markdown-remark@5.3.0) + '@notionhq/client': + specifier: ^2.2.15 + version: 2.2.15 '@radix-ui/react-slot': specifier: ^1.1.0 version: 1.1.0(@types/react@18.3.11)(react@18.3.1) @@ -867,6 +870,10 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} + '@notionhq/client@2.2.15': + resolution: {integrity: sha512-XhdSY/4B1D34tSco/GION+23GMjaS9S2zszcqYkMHo8RcWInymF6L1x+Gk7EmHdrSxNFva2WM8orhC4BwQCwgw==} + engines: {node: '>=12'} + '@oslojs/encoding@1.1.0': resolution: {integrity: sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==} @@ -1178,6 +1185,9 @@ packages: '@types/nlcst@2.0.3': resolution: {integrity: sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==} + '@types/node-fetch@2.6.11': + resolution: {integrity: sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==} + '@types/node@17.0.45': resolution: {integrity: sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==} @@ -2796,6 +2806,15 @@ packages: node-fetch-native@1.6.4: resolution: {integrity: sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ==} + node-fetch@2.7.0: + resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==} + engines: {node: 4.x || >=6.0.0} + peerDependencies: + encoding: ^0.1.0 + peerDependenciesMeta: + encoding: + optional: true + node-releases@2.0.18: resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} @@ -3572,6 +3591,9 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tr46@0.0.3: + resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@1.0.1: resolution: {integrity: sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA==} @@ -3891,6 +3913,9 @@ packages: web-namespaces@2.0.1: resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + webidl-conversions@3.0.1: + resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@4.0.2: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} @@ -3902,6 +3927,9 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@5.0.0: + resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} @@ -4750,6 +4778,13 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.17.1 + '@notionhq/client@2.2.15': + dependencies: + '@types/node-fetch': 2.6.11 + node-fetch: 2.7.0 + transitivePeerDependencies: + - encoding + '@oslojs/encoding@1.1.0': {} '@pagefind/darwin-arm64@1.1.1': @@ -5003,6 +5038,11 @@ snapshots: dependencies: '@types/unist': 3.0.3 + '@types/node-fetch@2.6.11': + dependencies: + '@types/node': 17.0.45 + form-data: 4.0.1 + '@types/node@17.0.45': {} '@types/normalize-package-data@2.4.4': {} @@ -7151,6 +7191,10 @@ snapshots: node-fetch-native@1.6.4: {} + node-fetch@2.7.0: + dependencies: + whatwg-url: 5.0.0 + node-releases@2.0.18: {} normalize-package-data@2.5.0: @@ -7988,6 +8032,8 @@ snapshots: totalist@3.0.1: {} + tr46@0.0.3: {} + tr46@1.0.1: dependencies: punycode: 2.3.1 @@ -8300,6 +8346,8 @@ snapshots: web-namespaces@2.0.1: {} + webidl-conversions@3.0.1: {} + webidl-conversions@4.0.2: {} whatwg-encoding@3.1.1: @@ -8308,6 +8356,11 @@ snapshots: whatwg-mimetype@4.0.0: {} + whatwg-url@5.0.0: + dependencies: + tr46: 0.0.3 + webidl-conversions: 3.0.1 + whatwg-url@7.1.0: dependencies: lodash.sortby: 4.7.0 diff --git a/src/components/planning/episode-card.astro b/src/components/planning/episode-card.astro new file mode 100644 index 00000000..c76ff02f --- /dev/null +++ b/src/components/planning/episode-card.astro @@ -0,0 +1,118 @@ +--- +import type { NotionEpisodeProperties } from "@/lib/notion/types"; + +const formatDate = (dateStr: string) => { + if (!dateStr) return ""; + return new Date(dateStr).toLocaleDateString("en-US", { + year: "numeric", + month: "long", + day: "numeric", + }); +}; + +interface Props { + episode: NotionEpisodeProperties; +} + +const { episode } = Astro.props as Props; +--- + +
+
+

+ {episode.title} +

+ +
+ { + episode.date && ( +
+ + + + {formatDate(episode.date)} +
+ ) + } + + { + episode.assignedTo && ( +
+ + + + {episode.assignedTo} +
+ ) + } +
+
+ +
+ + + +
+
diff --git a/src/components/planning/index.astro b/src/components/planning/index.astro new file mode 100644 index 00000000..facf9f45 --- /dev/null +++ b/src/components/planning/index.astro @@ -0,0 +1,75 @@ +--- +import { getGeekBlablaEpisodesPlannings } from "@/lib/notion"; +import EpisodeCard from "./episode-card.astro"; +const episodes = await getGeekBlablaEpisodesPlannings(); + +// Group episodes by status +const scheduled = episodes + .filter(ep => ep.status === "scheduled") + .sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); +const nextUp = episodes.filter(ep => ep.status.toLowerCase() === "next up"); +const backlog = episodes.filter(ep => ep.status === "backlog"); +--- + +
+
+

+ GeekBlabla Episodes Planning +

+
+ Track and manage upcoming episodes +
+ +
+ +
+
+

+ 📝Backlog +

+ + {backlog.length} + +
+
+ {backlog.map(episode => )} +
+
+ + +
+
+

+ 🎯Next Up +

+ + {nextUp.length} + +
+
+ {nextUp.map(episode => )} +
+
+ +
+
+

+ 📅Scheduled +

+ + {scheduled.length} + +
+
+ {scheduled.map(episode => )} +
+
+
+
+
diff --git a/src/lib/notion/index.ts b/src/lib/notion/index.ts new file mode 100644 index 00000000..b105a6a8 --- /dev/null +++ b/src/lib/notion/index.ts @@ -0,0 +1,120 @@ +import { Client } from "@notionhq/client"; +import type { + NotionResponse, + NotionNormalizedResponse, + NotionEpisodeCategory, + NotionEpisodeStatus, + NotionEpisodeProperties, + NotionEpisodeProperty, +} from "./types"; + +const databaseId = import.meta.env.GEEKSBLALA_NOTION_DATABASE_ID; +const apiKey = import.meta.env.NOTION_API_KEY; + +let notionClient: Client; + +const getNotionClient = () => { + if (!notionClient) { + notionClient = new Client({ auth: apiKey }); + } + return notionClient; +}; + +export async function getGeekBlablaEpisodesPlannings(): Promise { + if (import.meta.env.DEV) { + if (!apiKey || !databaseId) { + const data = await import("./notion-mock.json"); + return data.default as unknown as NotionNormalizedResponse; + } + } + + const episodes = (await getNotionClient().databases.query({ + database_id: databaseId, + filter: { + or: [ + { + property: "Status", + select: { + equals: "Backlog", + }, + }, + { + property: "Status", + select: { + equals: "Scheduled", + }, + }, + { + property: "Status", + select: { + equals: "Next Up", + }, + }, + ], + }, + })) as NotionResponse; + return normalizeNotionResponse(episodes); +} + +export function normalizeEpisodeProperties(properties: { + [key: string]: NotionEpisodeProperty; +}): NotionEpisodeProperties { + return { + title: + properties.title?.type === "title" + ? (properties.title.title[0]?.plain_text ?? "") + : "", + + date: + properties.Date?.type === "date" + ? (properties.Date.date?.start ?? "") + : "", + + guests: + properties.Guests?.type === "relation" + ? [] // Since guests are coming as empty relations in the sample + : [], + + description: + properties["Description "]?.type === "rich_text" + ? (properties["Description "].rich_text[0]?.plain_text ?? "") + : "", + + youtubeUrl: + properties["Youtube URL"]?.type === "url" + ? (properties["Youtube URL"].url ?? "") + : "", + + category: + properties["Category "]?.type === "select" + ? ((properties[ + "Category " + ].select?.name.toLowerCase() as NotionEpisodeCategory) ?? "dev") + : "dev", + + hosts: + properties.Hosts?.type === "relation" + ? [] // Since hosts are coming as empty relations in the sample + : [], + + assignedTo: + properties["Assign to "]?.type === "people" + ? (properties["Assign to "].people[0]?.name ?? "") + : "", + + status: + properties.Status?.type === "select" + ? ((properties.Status.select?.name.toLowerCase() as NotionEpisodeStatus) ?? + "backlog") + : "backlog", + }; +} + +export function normalizeNotionResponse( + response: NotionResponse +): NotionNormalizedResponse { + return response.results.map(page => { + const properties = page.properties; + return normalizeEpisodeProperties(properties); + }); +} diff --git a/src/lib/notion/notion-mock.json b/src/lib/notion/notion-mock.json new file mode 100644 index 00000000..c67a761a --- /dev/null +++ b/src/lib/notion/notion-mock.json @@ -0,0 +1,244 @@ +[ + { + "title": "Understanding LLMs from Scratch Using Middle School Math", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "next up" + }, + { + "title": "New Episode", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "next up" + }, + { + "title": "Robotics and Embedded systems ", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "AI in sport ", + "date": "2024-11-10", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "Mourad Mtouaa", + "status": "scheduled" + }, + { + "title": "AMA & Tech News ", + "date": "2024-11-24", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "scheduled" + }, + { + "title": "AI for good", + "date": "2024-11-17", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "ai", + "hosts": [], + "assignedTo": "Mohammed Daoudi", + "status": "scheduled" + }, + { + "title": "Fintech in Morocco ", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Startups in morocco", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Golang", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Women in Tech", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "وهم الإنتاجية وتحدي \n- `burnout`", + "date": "2024-11-03T20:00:00.000+01:00", + "guests": [], + "description": "السلام 👋حلقة جديدة الأحد القادم 🚩\n\nالحلقة تهم الجميع فيها غادي نتحدثتوا على الهوس الحديث بالإنتاجية و الأخطار بحال الإرهاق و تدهور الصحية النفسية\n\nغادي ناقشو الحلول كذلك باش نتقدموا فمساراتنا المهنية بدون منهدموا صحاتنا النفسية ✅\n\nكونوا فالموعد و متنساوش أن حضوركم فالتعليقات فالبث المباشر كيساهم فإغناء الدرشة", + "youtubeUrl": "https://www.youtube.com/watch?v=3JsNCcz1Hf0", + "category": "career", + "hosts": [], + "assignedTo": "Abdelati EL ASRI", + "status": "scheduled" + }, + { + "title": "New Episode based on twitter thread ", + "date": "2023-10-01", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "next up" + }, + { + "title": "Oracle Cloud Infrastructure: Deep Dive", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Azure Cloud: Deep Dive", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "software in 2030", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Quality Assurance ", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "MNCP episode ", + "date": "2022-12-04", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Legal things for the programmer", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Evolution of the web", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "Otmane Fettal", + "status": "backlog" + }, + { + "title": "Quality vs idea vs time managment ", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "dev", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "212 founders for developers", + "date": "", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "career", + "hosts": [], + "assignedTo": "", + "status": "backlog" + }, + { + "title": "Book ?", + "date": "2024-05-19", + "guests": [], + "description": "", + "youtubeUrl": "", + "category": "book", + "hosts": [], + "assignedTo": "Meriem Zaid", + "status": "next up" + } +] diff --git a/src/lib/notion/types.ts b/src/lib/notion/types.ts new file mode 100644 index 00000000..8f16a229 --- /dev/null +++ b/src/lib/notion/types.ts @@ -0,0 +1,196 @@ +interface NotionUser { + object: "user"; + id: string; + name?: string; + avatar_url?: string | null; + type: "person"; + person?: { + email: string; + }; +} + +interface NotionIcon { + type: "emoji"; + emoji: string; +} + +interface NotionParent { + type: "database_id"; + database_id: string; +} + +interface NotionProperty { + id: string; + type: string; +} + +interface NotionUrlProperty extends NotionProperty { + type: "url"; + url: string | null; +} + +interface NotionRelationProperty extends NotionProperty { + type: "relation"; + relation: unknown[]; + has_more: boolean; +} + +interface NotionCreatedByProperty extends NotionProperty { + type: "created_by"; + created_by: NotionUser; +} + +interface NotionRollupProperty extends NotionProperty { + type: "rollup"; + rollup: { + type: "array"; + array: unknown[]; + function: string; + }; +} + +interface NotionMultiSelectProperty extends NotionProperty { + type: "multi_select"; + multi_select: Array<{ + id: string; + name: string; + color: string; + }>; +} + +interface NotionSelectProperty extends NotionProperty { + type: "select"; + select: { + id: string; + name: string; + color: string; + } | null; +} + +interface NotionFormulaProperty extends NotionProperty { + type: "formula"; + formula: { + type: "string"; + string: string | null; + }; +} + +interface NotionPeopleProperty extends NotionProperty { + type: "people"; + people: NotionUser[]; +} + +interface NotionDateProperty extends NotionProperty { + type: "date"; + date: { + start: string; + end: string | null; + time_zone: string | null; + }; +} + +interface NotionRichTextProperty extends NotionProperty { + type: "rich_text"; + rich_text: Array<{ + type: "text"; + text: { + content: string; + link: string | null; + }; + annotations: { + bold: boolean; + italic: boolean; + strikethrough: boolean; + underline: boolean; + code: boolean; + color: string; + }; + plain_text: string; + href: string | null; + }>; +} + +interface NotionTitleProperty extends NotionProperty { + type: "title"; + title: Array<{ + type: "text"; + text: { + content: string; + link: string | null; + }; + annotations: { + bold: boolean; + italic: boolean; + strikethrough: boolean; + underline: boolean; + code: boolean; + color: string; + }; + plain_text: string; + href: string | null; + }>; +} + +export type NotionEpisodeProperty = + | NotionUrlProperty + | NotionRelationProperty + | NotionCreatedByProperty + | NotionRollupProperty + | NotionMultiSelectProperty + | NotionSelectProperty + | NotionFormulaProperty + | NotionPeopleProperty + | NotionDateProperty + | NotionRichTextProperty + | NotionTitleProperty; + +interface NotionPage { + object: "page"; + id: string; + created_time: string; + last_edited_time: string; + created_by: NotionUser; + last_edited_by: NotionUser; + cover: null; + icon: NotionIcon; + parent: NotionParent; + archived: boolean; + in_trash: boolean; + properties: { + [key: string]: NotionEpisodeProperty; + }; + url: string; + public_url: string; +} + +export interface NotionResponse { + object: "list"; + results: NotionPage[]; + next_cursor: string | null; + has_more: boolean; + type: "page_or_database"; + page_or_database: Record; + request_id: string; +} + +export type NotionEpisodeStatus = + | "scheduled" + | "next up" + | "backlog" + | "done" + | "archived"; +export type NotionEpisodeCategory = "dev" | "ai" | "ama" | "career" | "mss"; + +export type NotionEpisodeProperties = { + title: string; + date: string; + guests: string[]; + description: string; + youtubeUrl: string; + category: NotionEpisodeCategory; + hosts: string[]; + assignedTo: string; + status: NotionEpisodeStatus; +}; + +export type NotionNormalizedResponse = NotionEpisodeProperties[]; diff --git a/src/pages/planning.astro b/src/pages/planning.astro new file mode 100644 index 00000000..e85d6f79 --- /dev/null +++ b/src/pages/planning.astro @@ -0,0 +1,10 @@ +--- +import Header from "@/components/header.astro"; +import Layout from "@/components/layout.astro"; +import PlanningList from "@/components/planning/index.astro"; +--- + + +
+ + diff --git a/src/pages/podcast/[...slug].astro b/src/pages/podcast/[...slug].astro index 521e39cd..1f86199e 100644 --- a/src/pages/podcast/[...slug].astro +++ b/src/pages/podcast/[...slug].astro @@ -36,5 +36,12 @@ const episode = Astro.props; + + Planning +