Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions .agent/skills/news-system/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ Simply remove `news: true` from the post's frontmatter (or set it to `false`). T

**`GET /api/news`**

| Param | Default | Description |
|------------|---------|------------------------------------|
| `page` | `1` | Page number (1-indexed) |
| `pageSize` | `6` | Items per page (max 12) |
| Param | Default | Description |
| ---------- | ------- | ----------------------- |
| `page` | `1` | Page number (1-indexed) |
| `pageSize` | `6` | Items per page (max 12) |

The endpoint decodes HTML entities from Substack RSS (e.g., `’` → `'`).

Expand All @@ -72,6 +72,7 @@ The endpoint decodes HTML entities from Substack RSS (e.g., `’` → `'`).
## Substack Integration

The Substack feed is fetched server-side from `https://pauseai.substack.com/feed`. Items are parsed from RSS XML using regex extraction for:

- `<title>` → card title
- `<description>` → card subtitle
- `<link>` → card href
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"@fontsource/roboto-slab": "^5.2.8",
"@fontsource/saira-condensed": "^5.2.8",
"@glidejs/glide": "~3.6.2",
"@notionhq/client": "^2.2.15",
"@number-flow/svelte": "^0.3.13",
"@pagefind/default-ui": "^1.4.0",
"@prgm/sveltekit-progress-bar": "^2.0.0",
Expand Down
14 changes: 14 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions src/lib/components/LanguageSwitcher.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,7 @@
<Card>
<div class="list">
<!-- Add Auto-detect option at the top -->
<!-- eslint-disable-next-line svelte/no-restricted-html-elements -->
<a
href={deLocalizeHref($page.url.pathname)}
hreflang="auto"
Expand All @@ -179,6 +180,7 @@

{#each locales as locale}
{@const href = localizeHref($page.url.pathname, { locale })}
<!-- eslint-disable-next-line svelte/no-restricted-html-elements -->
<a
{href}
hreflang={locale}
Expand Down
1 change: 1 addition & 0 deletions src/lib/components/NewsCard.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@
opacity: 0.8;
display: -webkit-box;
-webkit-line-clamp: 2;
line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
Expand Down
2 changes: 2 additions & 0 deletions src/lib/components/PressLogos.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
<h2 class="section-title">Media Coverage</h2>
<div class="logos-row">
{#each publications as pub}
<!-- eslint-disable-next-line svelte/no-restricted-html-elements -->
<a href={pub.url} target="_blank" class="pub-link">
<!-- Visible on hover (Original Color) -->
<img
Expand All @@ -62,6 +63,7 @@
</a>
{/each}

<!-- eslint-disable-next-line svelte/no-restricted-html-elements -->
<a href="/press" class="see-all"> See all coverage → </a>
</div>
</div>
Expand Down
77 changes: 77 additions & 0 deletions src/lib/server/notion-people.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { getNotionClient } from './notion.js'
import type { Person } from '$lib/types'
import { defaultTitle } from '$lib/config'

export async function fetchNotionPeople(): Promise<Person[]> {
const notion = getNotionClient()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const results: any[] = []
let cursor: string | undefined = undefined

const dbId = '8948430b6a9940d5976581b71d9b3cd1'

try {
while (true) {
const response = await notion.databases.query({
database_id: dbId!,
start_cursor: cursor
})

// Append correctly based on the new SDK return type
if (response.results && Array.isArray(response.results)) {
results.push(...response.results)
}

if (!response.has_more) break
cursor = response.next_cursor || undefined
}

return results.map(notionPageToPerson)
} catch (error) {
console.error('Error fetching from Notion:', error)
throw error
}
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
function notionPageToPerson(page: any): Person {
const props = page.properties

const getText = (propName: string) => {
const prop = props[propName]
if (!prop) return ''
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (prop.type === 'title') return prop.title.map((t: any) => t.plain_text).join('')
// eslint-disable-next-line @typescript-eslint/no-explicit-any
if (prop.type === 'rich_text') return prop.rich_text.map((t: any) => t.plain_text).join('')
if (prop.type === 'select') return prop.select?.name || ''
return ''
}

const getCheckbox = (propName: string) => {
return props[propName]?.checkbox || false
}

const getNumber = (propName: string) => {
return props[propName]?.number ?? undefined
}

const getImage = (propName: string) => {
const files = props[propName]?.files
if (!files || files.length === 0) return undefined
const file = files[0]
return file.type === 'external' ? file.external.url : file.file.url
}

return {
id: page.id,
name: getText('Full name'),
bio: '',
title: getText('Title') || defaultTitle,
image: getImage('Photo') || getImage('Image'),
privacy: getCheckbox('Privacy'),
checked: getCheckbox('About') || getCheckbox('ABout'),
duplicate: getCheckbox('duplicate'),
order: getNumber('About order') ?? 999
}
}
14 changes: 14 additions & 0 deletions src/lib/server/notion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { Client } from '@notionhq/client'
import { env } from '$env/dynamic/private'

let notion: Client | null = null

export function getNotionClient() {
if (!notion) {
const auth = env.NOTION_API_KEY?.trim().replace(/^["']|["']$/g, '')
notion = new Client({
auth: auth
})
}
return notion
}
2 changes: 1 addition & 1 deletion src/posts/australia-detail.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
---
title: " PauseAI Australia (our campaigns)"
title: ' PauseAI Australia (our campaigns)'
slug: australia-detail
description: More information about the Australian chapter of PauseAI
---
Expand Down
11 changes: 11 additions & 0 deletions src/routes/about/+page.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,17 @@
{/each}
</section>

<section class="form-section" data-pagefind-ignore>
<iframe
src="https://pauseai-global.notion.site/ebd//318811439176805e9edef71080a593cb"
width="100%"
height="600"
frameborder="0"
allowfullscreen
title="Notion Form"
></iframe>
</section>

<section class="essential-info">
<h2>Essential Information</h2>
<ul class="essential-info-list">
Expand Down
56 changes: 9 additions & 47 deletions src/routes/api/about/+server.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
export const prerender = false

import { fetchAllPages } from '$lib/airtable'
import { defaultTitle } from '$lib/config'
import { fetchNotionPeople } from '$lib/server/notion-people'
import type { Person } from '$lib/types'
import { generateCacheControlRecord } from '$lib/utils'
import { json } from '@sveltejs/kit'
Expand All @@ -11,13 +7,13 @@ import { json } from '@sveltejs/kit'
export type AboutApiResponse = Record<string, Person[]>

/**
* Fallback people data to use in development if Airtable fetch fails
* Fallback people data to use in development if Notion fetch fails
*/
const fallbackPeople: Person[] = [
{
id: 'fallback-stub1',
name: '[FALLBACK DATA] Example Person',
bio: 'I hold places when Airtable API is unavailable.',
bio: '',
title: 'Placeholder',
image: 'https://api.dicebear.com/7.x/bottts/svg?seed=fallback1',
privacy: false,
Expand All @@ -27,7 +23,7 @@ const fallbackPeople: Person[] = [
{
id: 'fallback-stub2',
name: '[FALLBACK DATA] Holdor',
bio: 'Thrown at games',
bio: '',
title: 'of Plays',
image: 'https://api.dicebear.com/7.x/bottts/svg?seed=fallback2',
privacy: false,
Expand All @@ -36,24 +32,8 @@ const fallbackPeople: Person[] = [
}
]

function recordToPerson(record: any): Person {
return {
id: record.id || 'noId',
name: record.fields['Full name'],
bio: record.fields.Bio2,
title: record.fields.Title || defaultTitle,
image: (record.fields.Photo && record.fields.Photo[0].thumbnails.large.url) || undefined,
privacy: record.fields.Privacy,
checked: record.fields.About,
duplicate: record.fields.duplicate,
order: record.fields['About order'] || 999
}
}

const AIRTABLE_FILTER = `{Title} != ""`

const filter = (p: Person) => {
return p.checked && p.title?.trim() !== '' && p.title !== defaultTitle && !p.duplicate
return p.checked && !p.duplicate
}

const getGroupKey = (order: number | undefined): string => {
Expand All @@ -68,31 +48,13 @@ const getGroupKey = (order: number | undefined): string => {
return 'National Chapter Leads'
}

export async function GET({ fetch, setHeaders }) {
const url = `https://api.airtable.com/v0/appWPTGqZmUcs3NWu/tblL1icZBhTV1gQ9o`
export async function GET({ setHeaders }) {
setHeaders(generateCacheControlRecord({ public: true, maxAge: 60 * 60 }))

try {
// Create fallback records in the expected Airtable format
const fallbackRecords = fallbackPeople.map((person) => ({
id: person.id,
fields: {
'Full name': person.name,
Bio2: person.bio,
Title: person.title,
Photo: [{ thumbnails: { large: { url: person.image } } }],
Privacy: person.privacy,
About: person.checked, // Assuming 'About' maps to checked based on your code
'About order': person.order || 999
}
}))

const records = await fetchAllPages(fetch, url, fallbackRecords, {
filterByFormula: AIRTABLE_FILTER
})

const sortedPeople = records
.map(recordToPerson)
const people = await fetchNotionPeople()

const sortedPeople = people
.filter(filter)
.sort((a, b) => {
// Primary sort: numerical order field
Expand Down