Skip to content
Merged
1 change: 1 addition & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export const description =
export const url = dev ? 'http://localhost:37572' : 'https://pauseai.info'
export const botName = 'RogueGPT'
export const verificationParameter = 'verificationKey'
export const defaultTitle = 'Volunteer'
13 changes: 13 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@ export type AirtableSignatory = {
duplicate?: boolean
}

export type Person = {
id: string
name: string
/** URL to image file */
image?: string
bio: string
title?: string
/** Doesn't want to be visible on the /people page */
privacy?: boolean
checked?: boolean
duplicate?: boolean
}

export type Team = {
id: string
name: string
Expand Down
87 changes: 87 additions & 0 deletions src/routes/api/people/+server.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/* eslint-disable @typescript-eslint/no-explicit-any */
import type { Person } from '$lib/types'
import { defaultTitle } from '$lib/config'
import { json } from '@sveltejs/kit'
import { fetchAllPages } from '$lib/airtable'

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

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,
privacy: record.fields.Privacy,
checked: record.fields.About,
duplicate: record.fields.duplicate
}
}

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

export async function GET({ fetch, setHeaders }) {
const url = `https://api.airtable.com/v0/appWPTGqZmUcs3NWu/tblL1icZBhTV1gQ9o`
setHeaders({
'cache-control': 'public, max-age=3600' // 1 hour in seconds
})

try {
// Create fallback records in the expected Airtable format
const fallbackRecords = fallbackPeople.map((person) => ({
id: person.id,
fields: {
Name: person.name,
bio: person.bio,
title: person.title,
image: [{ thumbnails: { large: { url: person.image } } }],
privacy: person.privacy,
checked: person.checked
}
}))

const records = await fetchAllPages(fetch, url, fallbackRecords)
const out: Person[] = records
.map(recordToPerson)
.filter(filter)
// Shuffle the array, although not truly random
.sort(() => 0.5 - Math.random())
return json(out)
} catch (e) {
console.error('Error fetching people:', e)
// Return fallback data instead of error
return json(fallbackPeople.filter(filter))
}
}
2 changes: 2 additions & 0 deletions src/routes/api/posts/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { communitiesMeta } from '../../communities/communities'
import { meta as pdoomMeta } from '../../pdoom/meta'
import { meta as quotesMeta } from '../../quotes/meta'
import { meta as emailBuilderMeta } from '../../email-builder/meta'
import { meta as peopleMeta } from '../../people/meta'
import { meta as teamsMeta } from '../../teams/meta'
import { meta as statementMeta } from '../../statement/meta'
import { meta as dearSirDemisMeta } from '../../dear-sir-demis-2025/meta'
Expand All @@ -16,6 +17,7 @@ const hardCodedPages: Post[] = [
pdoomMeta,
quotesMeta,
emailBuilderMeta,
peopleMeta,
teamsMeta,
statementMeta,
dearSirDemisMeta
Expand Down
30 changes: 30 additions & 0 deletions src/routes/people/+page.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<script lang="ts">
import PostMeta from '$lib/components/PostMeta.svelte'
import { meta } from './meta'
import Person from './person.svelte'
export let data
const { people } = data
const { title, description, date } = meta
</script>

<PostMeta {title} {description} {date} />

<h1>{title}</h1>

<section data-pagefind-ignore>
{#if people.length === 0}
<p>No team members found</p>
{/if}
<ul class="people">
{#each people as { name, image, bio, title }}
<Person {name} {image} {bio} {title} />
{/each}
</ul>
</section>

<style>
.people {
display: grid;
gap: 1rem;
}
</style>
21 changes: 21 additions & 0 deletions src/routes/people/+page.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Person } from '$lib/types'
import { defaultTitle } from '$lib/config'

export const prerender = false

export const load = async ({ fetch, setHeaders }) => {
const response = await fetch('api/people')
const people: Person[] = await response.json()
setHeaders({
'cache-control': 'public, max-age=3600' // 1 hour in seconds
})
// sort people, those who dont have "Volunteer" as title should be at the top
people.sort((a, b) => {
if (a.title === defaultTitle && b.title !== defaultTitle) return 1
if (a.title !== defaultTitle && b.title === defaultTitle) return -1
return 0
})
return {
people: people
}
}
9 changes: 9 additions & 0 deletions src/routes/people/meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { Post } from '$lib/types'

export const meta: Post = {
title: 'People of PauseAI',
description: 'Staff and contributors behind PauseAI',
date: '2025-11-06',
slug: 'people',
categories: []
}
114 changes: 114 additions & 0 deletions src/routes/people/person.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<script lang="ts">
export let image: string | undefined
export let bio: string | undefined
export let name: string | undefined
export let title: string | undefined
const len = 60
let bioOpen = false
let bioTruncated = bio?.substring(0, len)
</script>

<li class="person">
{#if image}
<div class="image" style="background-image: url({image})"></div>
{/if}
<div class="details">
<div class="name-title">
<div class="name">
{name}
</div>
{#if title}
<div class="title">{title}</div>
{/if}
</div>
{#if bio}
<div class="bio">
{bioOpen ? bio : bioTruncated}
{#if !bioOpen && bio.length > len}
<button on:click={() => (bioOpen = !bioOpen)}>...</button>
{/if}
</div>
{/if}
</div>
</li>

<style>
.person {
display: flex;
gap: 1rem;
}
.details {
flex-shrink: 1;
}
.image {
display: flex;
flex-basis: 100px;
height: 100px;
align-items: center;
flex-shrink: 0;
justify-content: center;
border-radius: 50%;
background-size: cover;
background-position: center;
}
.name-title {
display: flex;
flex-direction: row;
align-items: center;
gap: 0.5rem;
}
@media (max-width: 600px) {
.name-title {
flex-direction: column;
align-items: flex-start;
gap: 0;
margin-bottom: 0.5rem;
}
}
.bio {
font-style: italic;
position: relative;
font-size: 0.9rem;
}
.bio button {
background: none;
border: none;
color: var(--brand);
cursor: pointer;
text-decoration: underline;
font-size: 0.9rem;
margin-left: -0.5rem;
}
.bio button:hover {
color: var(--brand-dark);
background-color: var(--bg-subtle);
border-radius: 6px;
}
.bio::before {
content: '"';
position: absolute;
/* make it pretty */
color: var(--text);
opacity: 0.1;
font-weight: bold;
font-size: 4rem;
left: -1.5rem;
top: 1.2rem;
line-height: 0;
margin-right: 0.5rem;
}
.name {
color: var(--text);
font-family: var(--font-heading);
font-weight: bold;
text-decoration: none;
font-size: 1.4rem;
text-transform: capitalize;
margin: 0;
}
.title {
font-weight: normal;
font-family: var(--font-body);
font-size: 1rem;
}
</style>
Loading