diff --git a/.env.example b/.env.example index 68ebd2c..5f9fa59 100644 --- a/.env.example +++ b/.env.example @@ -3,3 +3,7 @@ MAILGUN_API_KEY= MAILGUN_DOMAIN= MAILGUN_API_HOST= MAILGUN_TO_EMAIL= + +# Notion settings +NOTION_API_KEY= +NOTION_APPLICATION_FORMS_PAGE_ID= diff --git a/next.config.mjs b/next.config.mjs index 4678774..b3e778f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + domains: ['prod-files-secure.s3.us-west-2.amazonaws.com'], + }, +}; export default nextConfig; diff --git a/package.json b/package.json index c08429c..d73ccd6 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,8 @@ "mailgun-js": "^0.22.0", "next": "14.2.13", "react": "^18", - "react-dom": "^18" + "react-dom": "^18", + "react-query": "^3.39.3" }, "devDependencies": { "@types/node": "^20", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2f35cf9..d1e5a6e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: react-dom: specifier: ^18 version: 18.3.1(react@18.3.1) + react-query: + specifier: ^3.39.3 + version: 3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) devDependencies: '@types/node': specifier: ^20 @@ -58,6 +61,10 @@ packages: resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} + '@babel/runtime@7.25.7': + resolution: {integrity: sha512-FjoyLe754PMiYsFaN5C94ttGiOmBNYTf6pLr4xXHAT5uctHb092PBszndLDR5XA/jghQvn4n7JMHl7dmTgbm9w==} + engines: {node: '>=6.9.0'} + '@eslint-community/eslint-utils@4.4.0': resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} @@ -391,6 +398,10 @@ packages: balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + big-integer@1.6.52: + resolution: {integrity: sha512-QxD8cf2eVqJOOz63z6JIN9BzvVs/dlySa5HGSBH5xtR8dPteIRQnBxxKqkNTiT6jbDTF6jAfrd4oMcND9RGbQg==} + engines: {node: '>=0.6'} + binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -405,6 +416,9 @@ packages: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} + broadcast-channel@3.7.0: + resolution: {integrity: sha512-cIAKJXAxGJceNZGTZSBzMxzyOn72cVgPnKx4dc6LRjQgbaJUQqhy5rzL3zbMxkMWsGKkv2hSFkPRMEXfoMZ2Mg==} + busboy@1.6.0: resolution: {integrity: sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==} engines: {node: '>=10.16.0'} @@ -553,6 +567,9 @@ packages: resolution: {integrity: sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==} engines: {node: '>= 0.8'} + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + didyoumean@1.2.2: resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==} @@ -1101,6 +1118,9 @@ packages: resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==} hasBin: true + js-sha3@0.8.0: + resolution: {integrity: sha512-gF1cRrHhIzNfToc802P800N8PpXS+evLLXfsVpowqmAFR9uwbi89WvXg2QspOmXL8QL86J4T1EpFu+yUkwJY3Q==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} @@ -1179,6 +1199,9 @@ packages: engines: {node: '>=6.0.0'} deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + match-sorter@6.3.4: + resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -1187,6 +1210,9 @@ packages: resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} engines: {node: '>=8.6'} + microseconds@0.2.0: + resolution: {integrity: sha512-n7DHHMjR1avBbSpsTBj6fmMGh2AGrifVV4e+WYc3Q9lO+xnSZ3NyhcBND3vzzatt05LFhoKFRxrIyklmLlUtyA==} + mime-db@1.52.0: resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} engines: {node: '>= 0.6'} @@ -1218,6 +1244,9 @@ packages: mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nano-time@1.0.0: + resolution: {integrity: sha512-flnngywOoQ0lLQOTRNexn2gGSNuM9bKj9RZAWSzhQ+UJYaAFG9bac4DW9VHjUAzrOaIcajHybCTHe/bkvozQqA==} + nanoid@3.3.7: resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -1292,6 +1321,9 @@ packages: resolution: {integrity: sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==} engines: {node: '>= 0.4'} + oblivious-set@1.0.0: + resolution: {integrity: sha512-z+pI07qxo4c2CulUHCDf9lcqDlMSo72N/4rLUpRXf6fu+q8vjt8y0xS+Tlf8NTJDdTXHbdeO1n3MlbctwEoXZw==} + once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1514,6 +1546,18 @@ packages: react-is@16.13.1: resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + react-query@3.39.3: + resolution: {integrity: sha512-nLfLz7GiohKTJDuT4us4X3h/8unOh+00MLb2yJoGTPjxKs2bc1iDhkNx2bd5MKklXnOD3NrVZ+J2UXujA5In4g==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + react@18.3.1: resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} engines: {node: '>=0.10.0'} @@ -1535,10 +1579,16 @@ packages: resolution: {integrity: sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==} engines: {node: '>= 0.4'} + regenerator-runtime@0.14.1: + resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + regexp.prototype.flags@1.5.2: resolution: {integrity: sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==} engines: {node: '>= 0.4'} + remove-accents@0.5.0: + resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -1816,6 +1866,9 @@ packages: undici-types@6.19.8: resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + unload@2.2.0: + resolution: {integrity: sha512-B60uB5TNBLtN6/LsgAf3udH9saB5p7gqJwcFfbOEZ8BcBHnGwCf6G/TGiEqkRAxX7zAFIUtzdrXQSdL3Q/wqNA==} + unpipe@1.0.0: resolution: {integrity: sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==} engines: {node: '>= 0.8'} @@ -1884,6 +1937,10 @@ snapshots: '@alloc/quick-lru@5.2.0': {} + '@babel/runtime@7.25.7': + dependencies: + regenerator-runtime: 0.14.1 + '@eslint-community/eslint-utils@4.4.0(eslint@8.57.1)': dependencies: eslint: 8.57.1 @@ -2239,6 +2296,8 @@ snapshots: balanced-match@1.0.2: {} + big-integer@1.6.52: {} + binary-extensions@2.3.0: {} brace-expansion@1.1.11: @@ -2254,6 +2313,17 @@ snapshots: dependencies: fill-range: 7.1.1 + broadcast-channel@3.7.0: + dependencies: + '@babel/runtime': 7.25.7 + detect-node: 2.1.0 + js-sha3: 0.8.0 + microseconds: 0.2.0 + nano-time: 1.0.0 + oblivious-set: 1.0.0 + rimraf: 3.0.2 + unload: 2.2.0 + busboy@1.6.0: dependencies: streamsearch: 1.1.0 @@ -2404,6 +2474,8 @@ snapshots: depd@2.0.0: {} + detect-node@2.1.0: {} + didyoumean@1.2.2: {} dlv@1.1.3: {} @@ -3128,6 +3200,8 @@ snapshots: jiti@1.21.6: {} + js-sha3@0.8.0: {} + js-tokens@4.0.0: {} js-yaml@4.1.0: @@ -3209,6 +3283,11 @@ snapshots: transitivePeerDependencies: - supports-color + match-sorter@6.3.4: + dependencies: + '@babel/runtime': 7.25.7 + remove-accents: 0.5.0 + merge2@1.4.1: {} micromatch@4.0.8: @@ -3216,6 +3295,8 @@ snapshots: braces: 3.0.3 picomatch: 2.3.1 + microseconds@0.2.0: {} + mime-db@1.52.0: {} mime-types@2.1.35: @@ -3244,6 +3325,10 @@ snapshots: object-assign: 4.1.1 thenify-all: 1.6.0 + nano-time@1.0.0: + dependencies: + big-integer: 1.6.52 + nanoid@3.3.7: {} natural-compare@1.4.0: {} @@ -3322,6 +3407,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.0.0 + oblivious-set@1.0.0: {} + once@1.4.0: dependencies: wrappy: 1.0.2 @@ -3505,6 +3592,15 @@ snapshots: react-is@16.13.1: {} + react-query@3.39.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.25.7 + broadcast-channel: 3.7.0 + match-sorter: 6.3.4 + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) + react@18.3.1: dependencies: loose-envify: 1.4.0 @@ -3544,6 +3640,8 @@ snapshots: globalthis: 1.0.4 which-builtin-type: 1.1.4 + regenerator-runtime@0.14.1: {} + regexp.prototype.flags@1.5.2: dependencies: call-bind: 1.0.7 @@ -3551,6 +3649,8 @@ snapshots: es-errors: 1.3.0 set-function-name: 2.0.2 + remove-accents@0.5.0: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -3872,6 +3972,11 @@ snapshots: undici-types@6.19.8: {} + unload@2.2.0: + dependencies: + '@babel/runtime': 7.25.7 + detect-node: 2.1.0 + unpipe@1.0.0: {} uri-js@4.4.1: diff --git a/src/app/api/get-application-forms/[id]/route.ts b/src/app/api/get-application-forms/[id]/route.ts new file mode 100644 index 0000000..7758215 --- /dev/null +++ b/src/app/api/get-application-forms/[id]/route.ts @@ -0,0 +1,54 @@ +import { type NextRequest } from 'next/server'; + +import { + fetchApplicationFormsPageData, + fetchAllPageBlocks, + fetchApplicationFormsData, +} from '@/app/helpers/notionHelper'; + +export async function GET( + request: NextRequest, + { params }: { params: Promise<{ id: string }> }, +) { + const { id } = await params; + + try { + // First fetch page data + const pageData = await fetchApplicationFormsPageData(id); + + // Fetch all blocks (content) of the page + const pageBlocks = await fetchAllPageBlocks(id); + + // Check if block contains a child database + for (const block of pageBlocks.results) { + if (block && block.type === 'child_database') { + if (block.id) { + block.child_database_data = await fetchApplicationFormsData(block.id); + } + } + } + + // Combine both page data and blocks into one object + const combinedData = { + pageData, + pageBlocks, + }; + + // Return the combined data in the response + return new Response(JSON.stringify(combinedData), { + status: 200, + headers: { 'Content-Type': 'application/json' }, + }); + } catch (error) { + console.error('Error fetching page data or blocks:', error); + return new Response( + JSON.stringify({ + error: 'Failed to fetch data for page and page blocks from Notion', + }), + { + status: 500, + headers: { 'Content-Type': 'application/json' }, + }, + ); + } +} diff --git a/src/app/api/get-application-forms/route.ts b/src/app/api/get-application-forms/route.ts new file mode 100644 index 0000000..c9c1dac --- /dev/null +++ b/src/app/api/get-application-forms/route.ts @@ -0,0 +1,16 @@ +import { NextResponse } from 'next/server'; + +import { fetchApplicationFormsData } from '@/app/helpers/notionHelper'; + +export async function GET() { + try { + const pageId = process.env.NOTION_APPLICATION_FORMS_PAGE_ID || ''; + const data = await fetchApplicationFormsData(pageId); + return NextResponse.json(data, { status: 200 }); + } catch (error) { + return NextResponse.json( + { message: (error as Error).message }, + { status: 500 }, + ); + } +} diff --git a/src/app/blog/[slug]/[id]/page.tsx b/src/app/blog/[slug]/[id]/page.tsx new file mode 100644 index 0000000..0253eeb --- /dev/null +++ b/src/app/blog/[slug]/[id]/page.tsx @@ -0,0 +1,165 @@ +'use client'; + +import { notFound } from 'next/navigation'; +import { useQuery } from 'react-query'; +import Image from 'next/image'; + +import Container from '@/app/components/Container'; +import RenderNotionTableList from '@/app/components/notionRenders/RenderNotionTableList'; +import RenderNotionHeading1 from '@/app/components/notionRenders/RenderNotionHeading1'; +import RenderNotionHeading2 from '@/app/components/notionRenders/RenderNotionHeading2'; +import RenderNotionHeading3 from '@/app/components/notionRenders/RenderNotionHeading3'; +import RenderNotionParagraphs from '@/app/components/notionRenders/RenderNotionParagraphs'; +import RenderNotionParagraph from '@/app/components/notionRenders/RenderNotionParagraph'; +import RenderNotionImage from '@/app/components/notionRenders/RenderNotionImage'; + +async function fetchPageDetails(id: string) { + const res = await fetch(`/api/get-application-forms/${id}`); + return res.json(); +} + +interface Result { + id: string; + type: string; + heading_1?: { rich_text: { text: { content: string } }[] }; + heading_2?: { rich_text: { text: { content: string } }[] }; + heading_3?: { rich_text: { text: { content: string } }[] }; + paragraph?: { + rich_text: { + text: { + content: string; + link?: { + url: string; + }; + }; + annotations: { + bold: boolean; + }; + }[]; + }; + image?: { file: { url: string } }; + child_database_data?: { object: string; results: Result[] }; + properties?: { + [key: string]: { + title: Array<{ + plain_text: string; + }>; + }; + }; +} + +export default function BlogPost({ + params, +}: { + params: { slug: string; id: string }; +}) { + const id = params.id; + + const { data, error, isLoading } = useQuery( + ['applicationForm', id], + () => fetchPageDetails(id), + { enabled: !!id }, + ); + + if (isLoading) { + return ( +
+ Loading... +
+ ); + } + + if (error) { + const errorMessage = (error as Error).message; + console.error(errorMessage); + + notFound(); + } + + return ( + <> + {data.pageData.cover?.file?.url && ( + + )} + + {data.pageData.properties && ( +

+ {data.pageData.properties.Name.title[0].plain_text} +

+ )} + {data.pageData.properties.Date && ( +

+ {data.pageData.properties.Date.formula.string} +

+ )} +
+ {data.pageBlocks && + data.pageBlocks?.results.map((result: Result) => ( +
+ {result.type === 'heading_1' && ( + + )} + {result.type === 'heading_2' && ( + + )} + {result.type === 'heading_3' && ( + + )} + {result.type === 'paragraph' && + result.paragraph && + result.paragraph.rich_text?.length > 1 ? ( + + ) : ( + result.type === 'paragraph' && ( + + ) + )} + {result.type === 'image' && ( + + )} + {result.type === 'child_database' && + result.child_database_data && + result.child_database_data.object === 'list' && ( + ({ + ...r, + properties: r.properties || {}, + })), + }} + /> + )} +
+ ))} +
+
+ + ); +} diff --git a/src/app/blog/layout.tsx b/src/app/blog/layout.tsx new file mode 100644 index 0000000..1bbd604 --- /dev/null +++ b/src/app/blog/layout.tsx @@ -0,0 +1,15 @@ +'use client'; + +import { QueryClient, QueryClientProvider } from 'react-query'; + +const queryClient = new QueryClient(); + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + {children} + ); +} diff --git a/src/app/blog/page.tsx b/src/app/blog/page.tsx new file mode 100644 index 0000000..0499a7a --- /dev/null +++ b/src/app/blog/page.tsx @@ -0,0 +1,92 @@ +'use client'; + +import Link from 'next/link'; +import React from 'react'; +import { useQuery } from 'react-query'; +import Image from 'next/image'; +import { notFound } from 'next/navigation'; + +import { generateSlug } from '@/app/helpers/commons'; +import Container from '@/app/components/Container'; + +async function fetchApplicationForms() { + const res = await fetch('/api/get-application-forms'); + if (!res.ok) { + throw new Error('Failed to fetch application forms'); + } + return res.json(); +} + +interface Result { + id: string; + cover?: { + file?: { + url: string; + }; + }; + properties: { + Name: { + title: { + plain_text: string; + }[]; + }; + }; +} + +export default function Blog() { + const { data, error, isLoading } = useQuery( + 'applicationForms', + fetchApplicationForms, + ); + + if (isLoading) { + return ( +
+ Loading... +
+ ); + } + + if (error) { + const errorMessage = (error as Error).message; + console.error(errorMessage); + + notFound(); + } + + return ( + +

+ Blog +

+
+ {data?.results?.map((result: Result) => ( +
+ {result.cover?.file?.url && ( + + + + )} +

+ + {result.properties.Name.title[0].plain_text} + +

+
+ ))} +
+
+ ); +} diff --git a/src/app/components/notionRenders/RenderNotionHeading1.tsx b/src/app/components/notionRenders/RenderNotionHeading1.tsx new file mode 100644 index 0000000..303d1e7 --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionHeading1.tsx @@ -0,0 +1,7 @@ +export default function RenderNotionHeading1({ heading }: { heading: string }) { + if (heading.length === 0) { + return null; + } + + return

{heading}

; +} diff --git a/src/app/components/notionRenders/RenderNotionHeading2.tsx b/src/app/components/notionRenders/RenderNotionHeading2.tsx new file mode 100644 index 0000000..d3fc118 --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionHeading2.tsx @@ -0,0 +1,7 @@ +export default function RenderNotionHeading2({ heading }: { heading: string }) { + if (heading.length === 0) { + return null; + } + + return

{heading}

; +} diff --git a/src/app/components/notionRenders/RenderNotionHeading3.tsx b/src/app/components/notionRenders/RenderNotionHeading3.tsx new file mode 100644 index 0000000..a4cb1c0 --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionHeading3.tsx @@ -0,0 +1,7 @@ +export default function RenderNotionHeading3({ heading }: { heading: string }) { + if (heading.length === 0) { + return null; + } + + return

{heading}

; +} diff --git a/src/app/components/notionRenders/RenderNotionImage.tsx b/src/app/components/notionRenders/RenderNotionImage.tsx new file mode 100644 index 0000000..f59d5fd --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionImage.tsx @@ -0,0 +1,19 @@ +import Image from 'next/image'; + +export default function RenderNotionImage({ imageUrl }: { imageUrl: string }) { + if (imageUrl.length === 0) { + return null; + } + + return ( +
+ +
+ ); +} diff --git a/src/app/components/notionRenders/RenderNotionParagraph.tsx b/src/app/components/notionRenders/RenderNotionParagraph.tsx new file mode 100644 index 0000000..4d82e16 --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionParagraph.tsx @@ -0,0 +1,11 @@ +export default function RenderNotionParagraph({ + paragraph, +}: { + paragraph: string; +}) { + if (paragraph.length === 0) { + return null; + } + + return

{paragraph}

; +} diff --git a/src/app/components/notionRenders/RenderNotionParagraphs.tsx b/src/app/components/notionRenders/RenderNotionParagraphs.tsx new file mode 100644 index 0000000..4aa57bf --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionParagraphs.tsx @@ -0,0 +1,47 @@ +type Paragraph = { + text: { + content: string; + link?: { + url: string; + }; + }; + annotations: { + bold: boolean; + }; +}; + +export default function RenderNotionParagraphs({ + paragraphs, +}: { + paragraphs: Paragraph[]; +}) { + if (paragraphs.length === 0) { + return null; + } + + return ( +
+ {paragraphs.map((text: Paragraph, idx: number) => ( + + {text.text && text.text.link ? ( + + {text.text.content} + + ) : ( + + {text.text?.content} + + )} + + ))} +
+ ); +} diff --git a/src/app/components/notionRenders/RenderNotionTableList.tsx b/src/app/components/notionRenders/RenderNotionTableList.tsx new file mode 100644 index 0000000..37a163b --- /dev/null +++ b/src/app/components/notionRenders/RenderNotionTableList.tsx @@ -0,0 +1,116 @@ +interface TableData { + results: Array<{ + id: string; + properties: { + [key: string]: { + title: Array<{ + plain_text: string; + }>; + }; + }; + }>; +} + +export default function RenderNotionTableList({ + tableData, +}: { + tableData: TableData; +}) { + if (!Array.isArray(tableData) || tableData.length === 0) { + console.error('tableData is not an array or is empty', tableData); + return null; + } + + // Fetch the first result's properties to use as headers, if available + const headers = + tableData && tableData.length > 0 + ? Object.keys(tableData[0].properties) + : []; + + if (headers.length === 0) { + return null; + } + + return ( +
+ + + + {headers.map((header, index) => ( + + ))} + + + + {tableData.map(item => ( + + {Object.keys(item.properties).map(key => { + const property = item.properties[key]; + + // Check if the property is null or undefined + if (property === null || property === undefined) { + return ( + + ); + } + + // Handle different property types + switch (property.type) { + case 'number': + return ( + + ); + + case 'title': + return ( + + ); + + case 'multi_select': + return ( + + ); + + case 'relation': + return ( + + ); + + // Add more cases as needed based on property types + default: + return ( + + ); + } + })} + + ))} + +
+ {header} +
+ {' '} + + {property.number !== null + ? '€ ' + property.number + : ' '} + + {property.title.length > 0 + ? property.title[0].plain_text + : ' '} + + {property.multi_select.length > 0 + ? property.multi_select + .map((option: { name: string }) => option.name) + .join(', ') + : ' '} + + {property.relation.length > 0 + ? property.relation.map((rel: { id: string }) => rel.id).join(', ') + : ' '} + + {' '} +
+
+ ); +} diff --git a/src/app/config/index.ts b/src/app/config/index.ts index 2d01f9d..a3fa639 100644 --- a/src/app/config/index.ts +++ b/src/app/config/index.ts @@ -9,3 +9,6 @@ export const docsLink = 'https://docs.muqa.org'; export const githubLink = 'https://github.com/muqa-org'; export const twitterLink = 'https://x.com/muqaorg'; export const telegramLink = 'https://t.me/muqaorg'; + +// Nnotion blog links +export const notionAPIURL = 'https://api.notion.com/v1/'; diff --git a/src/app/globals.css b/src/app/globals.css index a772dd9..465f4e4 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -4,11 +4,13 @@ @media (max-width: 768px) { #zazelenimo { - background: url('/images/campaigns/zazelenimo-bg-mobile.png') center center no-repeat !important; - background-size: cover !important; + background: url('/images/campaigns/zazelenimo-bg-mobile.png') center center + no-repeat !important; + background-size: cover !important; } #hnd { - background: url('/images/campaigns/hnd-bg-mobile.png') center center no-repeat !important; - background-size: cover !important; + background: url('/images/campaigns/hnd-bg-mobile.png') center center + no-repeat !important; + background-size: cover !important; } } diff --git a/src/app/helpers/commons.ts b/src/app/helpers/commons.ts new file mode 100644 index 0000000..cc414ac --- /dev/null +++ b/src/app/helpers/commons.ts @@ -0,0 +1,21 @@ +/** + * Generates a URL-friendly slug from a given title string. + * + * This function: + * - Converts the title to lowercase + * - Trims any leading and trailing whitespace + * - Removes non-alphanumeric characters except spaces and hyphens + * - Replaces spaces with hyphens + * - Replaces multiple consecutive hyphens with a single hyphen + * + * @param {string} title - The title to convert into a slug. + * @returns {string} - The generated slug. + */ +export function generateSlug(title: string): string { + return title + .toLowerCase() // Convert to lowercase + .trim() // Trim whitespace from both ends + .replace(/[^\w\s-]/g, '') // Remove non-word characters except spaces and hyphens + .replace(/\s+/g, '-') // Replace spaces with hyphens + .replace(/-+/g, '-'); // Replace multiple hyphens with a single hyphen +} diff --git a/src/app/helpers/notionHelper.ts b/src/app/helpers/notionHelper.ts new file mode 100644 index 0000000..37cc7a8 --- /dev/null +++ b/src/app/helpers/notionHelper.ts @@ -0,0 +1,79 @@ +import { notionAPIURL } from '@/app/config'; + +/** + * Fetches data for the entire application forms database from Notion. + * Sends a POST request to the Notion API to query the specified database. + * Throws an error if the fetch operation fails. + * + * @param {string} pageId - The unique identifier of the Notion page to fetch. + * @returns {Promise} The JSON response containing the database data. + * @throws Will throw an error if the request fails. + */ +export async function fetchApplicationFormsData(pageId: string) { + const res = await fetch(`${notionAPIURL}databases/${pageId}/query`, { + method: 'POST', + headers: { + Authorization: `Bearer ${process.env.NOTION_API_KEY}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error('Failed to fetch data from Notion'); + } + + return res.json(); +} + +/** + * Fetches data for a specific page within the application forms database. + * Sends a GET request to the Notion API to retrieve page details by ID. + * Throws an error if the fetch operation fails. + * + * @param {string} pageId - The unique identifier of the Notion page to fetch. + * @returns {Promise} The JSON response containing the page data. + * @throws Will throw an error if the request fails. + */ +export async function fetchApplicationFormsPageData(pageId: string) { + const res = await fetch(`${notionAPIURL}pages/${pageId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${process.env.NOTION_API_KEY}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error('Failed to fetch data of the page from Notion'); + } + + return res.json(); +} + +/** + * Fetches all child blocks of a specific Notion page. + * Sends a GET request to the Notion API to retrieve all blocks within a page by page ID. + * Throws an error if the fetch operation fails. + * + * @param {string} pageId - The unique identifier of the Notion page whose blocks are to be fetched. + * @returns {Promise} The JSON response containing the page's child blocks. + * @throws Will throw an error if the request fails. + */ +export async function fetchAllPageBlocks(pageId: string) { + const res = await fetch(`${notionAPIURL}blocks/${pageId}/children`, { + method: 'GET', + headers: { + Authorization: `Bearer ${process.env.NOTION_API_KEY}`, + 'Notion-Version': '2022-06-28', + 'Content-Type': 'application/json', + }, + }); + + if (!res.ok) { + throw new Error('Failed to fetch data of the blocks from Notion'); + } + + return res.json(); +}