+11
-28
plugins/fluidType.ts
+11
-28
plugins/fluidType.ts
···
87
360,
88
1200,
89
],
90
// md: Medium text (body copy, paragraphs)
91
md: [
92
-
16,
93
24,
94
360,
95
1200,
96
],
97
// lg: Large text (subheadings, lead paragraphs)
98
lg: [
99
-
24,
100
48,
101
360,
102
1200,
103
],
104
// xl: Extra large text (headings)
105
xl: [
106
-
32,
107
56,
108
360,
109
1200,
···
115
360,
116
1200,
117
],
118
-
// Legacy presets for backward compatibility
119
-
h1: [
120
-
28,
121
-
88,
122
-
360,
123
-
1200,
124
-
],
125
-
h2: [
126
-
22,
127
-
48,
128
-
360,
129
-
1200,
130
-
],
131
-
h3: [
132
-
18,
133
-
32,
134
-
360,
135
-
1200,
136
-
],
137
-
body: [
138
-
16,
139
-
24,
140
-
360,
141
-
1200,
142
-
],
143
};
144
145
matchUtilities(
···
87
360,
88
1200,
89
],
90
+
// base: default text (body copy, paragraphs)
91
+
base: [
92
+
16,
93
+
32,
94
+
360,
95
+
1200,
96
+
],
97
// md: Medium text (body copy, paragraphs)
98
md: [
99
24,
100
+
40,
101
360,
102
1200,
103
],
104
// lg: Large text (subheadings, lead paragraphs)
105
lg: [
106
+
32,
107
48,
108
360,
109
1200,
110
],
111
// xl: Extra large text (headings)
112
xl: [
113
+
40,
114
56,
115
360,
116
1200,
···
122
360,
123
1200,
124
],
125
+
126
};
127
128
matchUtilities(
+3
-1
src/App.tsx
+3
-1
src/App.tsx
···
1
import AboutPage from '@/pages/AboutPage';
2
import HomePage from '@/pages/HomePage';
3
-
import WorksPage from '@/pages/WorksPage';
4
import WritingPage from '@/pages/WritingPage';
5
import React, { useEffect } from 'react';
6
import { Route, Routes } from 'react-router-dom';
···
37
<Routes>
38
<Route path="/" element={<HomePage />} />
39
<Route path="/about" element={<AboutPage />} />
40
<Route path="/works" element={<WorksPage />} />
41
<Route path="/writing" element={<WritingPage />} />
42
{/* <Route path="/studies" element={<Studies />} /> */}
···
1
import AboutPage from '@/pages/AboutPage';
2
import HomePage from '@/pages/HomePage';
3
+
import WorksPage from '@/pages/PortfolioPage';
4
+
import WorkPage from '@/pages/WorkPage';
5
import WritingPage from '@/pages/WritingPage';
6
import React, { useEffect } from 'react';
7
import { Route, Routes } from 'react-router-dom';
···
38
<Routes>
39
<Route path="/" element={<HomePage />} />
40
<Route path="/about" element={<AboutPage />} />
41
+
<Route path="/work" element={<WorkPage />} />
42
<Route path="/works" element={<WorksPage />} />
43
<Route path="/writing" element={<WritingPage />} />
44
{/* <Route path="/studies" element={<Studies />} /> */}
+9
-9
src/components/CardArticle/CardArticle.tsx
+9
-9
src/components/CardArticle/CardArticle.tsx
···
19
20
{/* Content */}
21
<div className={styles.contentContainer}>
22
{/* Publication Name (left) and Date (right) */}
23
<div className={styles.metaContainer}>
24
<div className={styles.publicationContainer}>
···
26
<span className={styles.publicationName}>{article.publication}</span>
27
</div>
28
<span className={styles.date}>{formattedDate}</span>
29
-
</div>
30
-
<div className={styles.detailContainer}>
31
-
{/* Title */}
32
-
<Heading level={3} size="lg">
33
-
{article.title}
34
-
</Heading>
35
-
36
-
{/* Description */}
37
-
{article.subtitle && <Paragraph>{article.subtitle}</Paragraph>}
38
</div>
39
</div>
40
</a>
···
19
20
{/* Content */}
21
<div className={styles.contentContainer}>
22
+
<div className={styles.detailContainer}>
23
+
{/* Title */}
24
+
<Heading level={3} size="md">
25
+
{article.title}
26
+
</Heading>
27
+
28
+
{/* Description */}
29
+
{article.subtitle && <Paragraph size="base">{article.subtitle}</Paragraph>}
30
+
</div>
31
{/* Publication Name (left) and Date (right) */}
32
<div className={styles.metaContainer}>
33
<div className={styles.publicationContainer}>
···
35
<span className={styles.publicationName}>{article.publication}</span>
36
</div>
37
<span className={styles.date}>{formattedDate}</span>
38
</div>
39
</div>
40
</a>
+6
src/components/CardEvent/CardEvent.styles.ts
+6
src/components/CardEvent/CardEvent.styles.ts
···
···
1
+
export const cardEventStyles = {
2
+
wrapper: 'px-8 py-12 bg-bones-white dark:bg-bones-black flex flex-col gap-8',
3
+
title: 'text-2xl font-bold font-dm-sans text-neutral-900',
4
+
subtitle: 'text-lg font-normal font-dm-sans text-neutral-700 italic mt-2',
5
+
meta: 'text-sm font-medium font-dm-sans text-neutral-500 uppercase mt-4',
6
+
} as const;
+32
src/components/CardEvent/CardEvent.tsx
+32
src/components/CardEvent/CardEvent.tsx
···
···
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
+
import { cn } from '@/lib/utils';
4
+
import React from 'react';
5
+
import { cardEventStyles } from './CardEvent.styles';
6
+
import { CardEventProps } from './CardEvent.types';
7
+
8
+
export const CardEvent: React.FC<CardEventProps> = ({
9
+
eventType,
10
+
eventTitle,
11
+
eventDescription,
12
+
eventStartYear,
13
+
eventEndYear,
14
+
eventAffiliation,
15
+
className = '',
16
+
}) => (
17
+
<div className={cn(cardEventStyles.wrapper, className)}>
18
+
<div className="flex items-center justify-between">
19
+
<Paragraph size="sm">{eventAffiliation}</Paragraph>
20
+
<Paragraph size="sm">
21
+
{eventStartYear} – {eventEndYear}
22
+
</Paragraph>
23
+
</div>
24
+
<div className="flex flex-col gap-2">
25
+
<Heading level={3} size="lg">
26
+
{eventTitle}
27
+
</Heading>
28
+
<Paragraph size="md">{eventDescription}</Paragraph>
29
+
</div>
30
+
<Paragraph size="sm">{eventType}</Paragraph>
31
+
</div>
32
+
);
+9
src/components/CardEvent/CardEvent.types.ts
+9
src/components/CardEvent/CardEvent.types.ts
+3
src/components/CardFact/CardFact.styles.ts
+3
src/components/CardFact/CardFact.styles.ts
+15
src/components/CardFact/CardFact.tsx
+15
src/components/CardFact/CardFact.tsx
···
···
1
+
import { Heading } from '@/components/Heading/Heading';
2
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
+
import { cn } from '@/lib/utils';
4
+
import React from 'react';
5
+
import { cardFactStyles } from './CardFact.styles';
6
+
import { CardFactProps } from './CardFact.types';
7
+
8
+
export const CardFact: React.FC<CardFactProps> = ({ title, subtitle, className = '' }) => (
9
+
<div className={cn(cardFactStyles.wrapper, className)}>
10
+
<Heading level={3} size="lg">
11
+
{title}
12
+
</Heading>
13
+
<Paragraph size="md">{subtitle}</Paragraph>
14
+
</div>
15
+
);
+5
src/components/CardFact/CardFact.types.ts
+5
src/components/CardFact/CardFact.types.ts
+1
-1
src/components/CardNote/CardNote.tsx
+1
-1
src/components/CardNote/CardNote.tsx
+1
-1
src/components/CardRole/CardRole.styles.ts
+1
-1
src/components/CardRole/CardRole.styles.ts
···
1
export const cardWrapper =
2
-
'group relative flex flex-col h-full bg-bones-white dark:bg-bones-black text-left w-full cursor-pointer hover:bg-bones-white-10 dark:hover:bg-bones-black-10 transition-colors';
3
export const contentContainer = 'flex flex-col flex-grow gap-4 p-8 justify-between';
4
export const coverImage = 'object-cover w-full h-full transition-transform group-hover:scale-105';
5
export const coverImageContainer = 'relative aspect-[16/9] w-full overflow-hidden bg-neutral-100 dark:bg-neutral-700';
···
1
export const cardWrapper =
2
+
'group relative flex flex-col h-full bg-bones-white dark:bg-bones-black text-left w-full hover:bg-bones-white-10 dark:hover:bg-bones-black-10 transition-colors';
3
export const contentContainer = 'flex flex-col flex-grow gap-4 p-8 justify-between';
4
export const coverImage = 'object-cover w-full h-full transition-transform group-hover:scale-105';
5
export const coverImageContainer = 'relative aspect-[16/9] w-full overflow-hidden bg-neutral-100 dark:bg-neutral-700';
+24
-18
src/components/CardRole/CardRole.tsx
+24
-18
src/components/CardRole/CardRole.tsx
···
1
import { Heading } from '@/components/Heading/Heading';
2
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
import React from 'react';
4
-
import { useNavigate } from 'react-router-dom';
5
-
import { ROLES_BASE_PATH } from './CardRole.constants';
6
import * as styles from './CardRole.styles';
7
import { CardRoleProps } from './CardRole.types';
8
9
export const CardRole: React.FC<CardRoleProps> = ({ role }) => {
10
-
const navigate = useNavigate();
11
12
return (
13
-
<button onClick={() => navigate(`${ROLES_BASE_PATH}/${role.slug}`)} className={styles.cardWrapper}>
14
-
{/* Cover Image (if available) */}
15
-
{role.coverImage && (
16
-
<div className={styles.coverImageContainer}>
17
-
<img src={role.coverImage} alt="" className={styles.coverImage} />
18
-
</div>
19
-
)}
20
-
21
{/* Content */}
22
<div className={styles.contentContainer}>
23
{/* Company Name (left) and Date (right) */}
24
<div className={styles.metaContainer}>
25
<span className={styles.companyName}>{role.company}</span>
26
-
<span className={styles.date}>{role.date}</span>
27
</div>
28
<div className={styles.detailContainer}>
29
-
{/* Title */}
30
-
<Heading level={3} size="lg">
31
-
{role.title}
32
</Heading>
33
34
-
{/* Subtitle/Description */}
35
-
{role.subtitle && <Paragraph>{role.subtitle}</Paragraph>}
36
</div>
37
</div>
38
-
</button>
39
);
40
};
···
1
import { Heading } from '@/components/Heading/Heading';
2
import { Paragraph } from '@/components/Paragraph/Paragraph';
3
import React from 'react';
4
import * as styles from './CardRole.styles';
5
import { CardRoleProps } from './CardRole.types';
6
7
+
/**
8
+
* Format date string to readable format
9
+
*/
10
+
function formatDate(dateString: string): string {
11
+
const date = new Date(dateString);
12
+
if (isNaN(date.getTime())) return dateString;
13
+
14
+
return date.toLocaleDateString('en-US', {
15
+
year: 'numeric',
16
+
month: 'short',
17
+
});
18
+
}
19
+
20
export const CardRole: React.FC<CardRoleProps> = ({ role }) => {
21
+
const startDate = formatDate(role.startDate);
22
+
const endDate = role.endDate ? formatDate(role.endDate) : 'Present';
23
+
const dateRange = `${startDate} — ${endDate}`;
24
25
return (
26
+
<div className={styles.cardWrapper}>
27
{/* Content */}
28
<div className={styles.contentContainer}>
29
{/* Company Name (left) and Date (right) */}
30
<div className={styles.metaContainer}>
31
<span className={styles.companyName}>{role.company}</span>
32
+
<span className={styles.date}>{dateRange}</span>
33
</div>
34
<div className={styles.detailContainer}>
35
+
{/* Position Title */}
36
+
<Heading level={3} size="md">
37
+
{role.position}
38
</Heading>
39
40
+
{/* Description */}
41
+
{role.description && <Paragraph size="base">{role.description}</Paragraph>}
42
</div>
43
</div>
44
+
</div>
45
);
46
};
+3
-8
src/components/CardRole/CardRole.types.ts
+3
-8
src/components/CardRole/CardRole.types.ts
+2
-2
src/components/Divider/Divider.styles.ts
+2
-2
src/components/Divider/Divider.styles.ts
+1
-1
src/components/Divider/Divider.tsx
+1
-1
src/components/Divider/Divider.tsx
···
11
// Accent: white border
12
// Default: black border in light mode, white in dark
13
const themeClasses =
14
-
pageTheme === 'accent' ? 'border-bones-white' : 'border-bones-black-30 dark:border-bones-white-30';
15
16
const orientationStyles = orientation === 'vertical' ? dividerVertical : dividerHorizontal;
17
···
11
// Accent: white border
12
// Default: black border in light mode, white in dark
13
const themeClasses =
14
+
pageTheme === 'accent' ? 'border-bones-white' : 'border-bones-black-20 dark:border-bones-white-20';
15
16
const orientationStyles = orientation === 'vertical' ? dividerVertical : dividerHorizontal;
17
+2
-2
src/components/Events/Events.tsx
+2
-2
src/components/Events/Events.tsx
···
1
import { cn } from '@/lib/utils';
2
import React from 'react';
3
-
import { Event } from '@/components/Event/Event';
4
import { eventsStyles } from './Events.styles';
5
import { EventsProps } from './Events.types';
6
7
export const Events: React.FC<EventsProps> = ({ items, className = '' }) => (
8
<div className={cn(eventsStyles.wrapper, className)}>
9
{items.map((item) => (
10
-
<Event
11
key={item.eventTitle}
12
eventType={item.eventType}
13
eventTitle={item.eventTitle}
···
1
import { cn } from '@/lib/utils';
2
import React from 'react';
3
+
import { CardEvent } from '@/components/CardEvent/CardEvent';
4
import { eventsStyles } from './Events.styles';
5
import { EventsProps } from './Events.types';
6
7
export const Events: React.FC<EventsProps> = ({ items, className = '' }) => (
8
<div className={cn(eventsStyles.wrapper, className)}>
9
{items.map((item) => (
10
+
<CardEvent
11
key={item.eventTitle}
12
eventType={item.eventType}
13
eventTitle={item.eventTitle}
+2
-2
src/components/Events/Events.types.ts
+2
-2
src/components/Events/Events.types.ts
+2
-2
src/components/Facts/Facts.tsx
+2
-2
src/components/Facts/Facts.tsx
···
1
import { cn } from '@/lib/utils';
2
import React from 'react';
3
-
import { Fact } from '@/components/Fact/Fact';
4
import { factsStyles } from './Facts.styles';
5
import { FactsProps } from './Facts.types';
6
···
8
return (
9
<div className={cn(factsStyles.wrapper, className)}>
10
{items.map((item) => (
11
-
<Fact key={item.title} title={item.title} subtitle={item.subtitle} />
12
))}
13
</div>
14
);
···
1
import { cn } from '@/lib/utils';
2
import React from 'react';
3
+
import { CardFact } from '@/components/CardFact/CardFact';
4
import { factsStyles } from './Facts.styles';
5
import { FactsProps } from './Facts.types';
6
···
8
return (
9
<div className={cn(factsStyles.wrapper, className)}>
10
{items.map((item) => (
11
+
<CardFact key={item.title} title={item.title} subtitle={item.subtitle} />
12
))}
13
</div>
14
);
+2
-2
src/components/Facts/Facts.types.ts
+2
-2
src/components/Facts/Facts.types.ts
+1
src/components/Heading/Heading.styles.ts
+1
src/components/Heading/Heading.styles.ts
+1
-1
src/components/Heading/Heading.types.ts
+1
-1
src/components/Heading/Heading.types.ts
+4
-7
src/components/Layout/Layout.styles.ts
+4
-7
src/components/Layout/Layout.styles.ts
···
3
4
export const getLayoutStyles = (theme: PageTheme = 'default') =>
5
cn(
6
-
'grid grid-cols-1 md:grid-cols-3 min-h-screen gap-0',
7
// Accent theme: blue (light) / mediumblue (dark), white text
8
theme === 'accent' && 'bg-bones-blue dark:bg-bones-mediumblue text-bones-white',
9
// Default theme: white (light) / black (dark), black/white text
10
theme === 'default' && 'bg-bones-white dark:bg-bones-black text-bones-black dark:text-bones-white',
11
);
12
13
-
export const main = 'md:col-span-2 p-12 md:order-1';
14
15
export const getAsideStyles = (theme: PageTheme = 'default') =>
16
cn(
17
-
'md:col-span-1 p-12 md:order-2',
18
-
// Accent theme: white border with transparency
19
-
theme === 'accent' && 'border-l border-bones-white-20',
20
-
// Default theme: black/white borders based on mode
21
-
theme === 'default' && 'border-l border-bones-black-20 dark:border-bones-white-20',
22
);
···
3
4
export const getLayoutStyles = (theme: PageTheme = 'default') =>
5
cn(
6
+
'grid grid-cols-1 md:grid-cols-[2fr_auto_1fr] min-h-screen gap-0',
7
// Accent theme: blue (light) / mediumblue (dark), white text
8
theme === 'accent' && 'bg-bones-blue dark:bg-bones-mediumblue text-bones-white',
9
// Default theme: white (light) / black (dark), black/white text
10
theme === 'default' && 'bg-bones-white dark:bg-bones-black text-bones-black dark:text-bones-white',
11
);
12
13
+
export const main = 'p-12';
14
15
export const getAsideStyles = (theme: PageTheme = 'default') =>
16
cn(
17
+
'p-12',
18
+
// Theme-specific styles (border removed - now using Divider component)
19
);
+15
-1
src/components/Layout/Layout.tsx
+15
-1
src/components/Layout/Layout.tsx
···
2
import * as styles from './Layout.styles';
3
import { DEFAULT_THEME } from './Layout.constants';
4
import { AsideProps, LayoutProps, PageTheme, MainProps } from './Layout.types';
5
6
// Context to share page theme with child components
7
const PageThemeContext = createContext<PageTheme>(DEFAULT_THEME);
···
9
export const usePageTheme = () => useContext(PageThemeContext);
10
11
export const Layout: React.FC<LayoutProps> = ({ children, theme = DEFAULT_THEME }) => {
12
return (
13
<PageThemeContext.Provider value={theme}>
14
-
<div className={styles.getLayoutStyles(theme)}>{children}</div>
15
</PageThemeContext.Provider>
16
);
17
};
···
2
import * as styles from './Layout.styles';
3
import { DEFAULT_THEME } from './Layout.constants';
4
import { AsideProps, LayoutProps, PageTheme, MainProps } from './Layout.types';
5
+
import { Divider } from '@/components/Divider/Divider';
6
7
// Context to share page theme with child components
8
const PageThemeContext = createContext<PageTheme>(DEFAULT_THEME);
···
10
export const usePageTheme = () => useContext(PageThemeContext);
11
12
export const Layout: React.FC<LayoutProps> = ({ children, theme = DEFAULT_THEME }) => {
13
+
// Split children into Main and Aside
14
+
const childArray = React.Children.toArray(children);
15
+
const mainComponent = childArray.find(
16
+
(child) => React.isValidElement(child) && child.type === Main
17
+
);
18
+
const asideComponent = childArray.find(
19
+
(child) => React.isValidElement(child) && child.type === Aside
20
+
);
21
+
22
return (
23
<PageThemeContext.Provider value={theme}>
24
+
<div className={styles.getLayoutStyles(theme)}>
25
+
{mainComponent}
26
+
{asideComponent && <Divider orientation="vertical" className="hidden md:block" />}
27
+
{asideComponent}
28
+
</div>
29
</PageThemeContext.Provider>
30
);
31
};
+1
src/components/ListItem/ListItem.styles.ts
+1
src/components/ListItem/ListItem.styles.ts
+1
src/components/Paragraph/Paragraph.styles.ts
+1
src/components/Paragraph/Paragraph.styles.ts
+1
-1
src/components/Paragraph/Paragraph.types.ts
+1
-1
src/components/Paragraph/Paragraph.types.ts
+1
-1
src/components/Skills/Skills.types.ts
+1
-1
src/components/Skills/Skills.types.ts
+2
-1
src/components/TLDRProfile/TLDRProfile.constants.ts
+2
-1
src/components/TLDRProfile/TLDRProfile.constants.ts
+32
-10
src/components/TLDRProfile/TLDRProfile.tsx
+32
-10
src/components/TLDRProfile/TLDRProfile.tsx
···
1
import { Link } from '@/components/Link/Link';
2
import { Tags } from '@/components/Tags/Tags';
3
-
import { specializations } from '@/data/specializations';
4
import type { JSX } from 'react';
5
import { Heading } from '../Heading/Heading';
6
import { Paragraph } from '../Paragraph/Paragraph';
···
13
* @returns JSX element with profile photo and summary information
14
*/
15
export default function TLDRProfile(): JSX.Element {
16
return (
17
<div className={styles.container}>
18
<div className={styles.innerContainer}>
···
22
<Heading level={3} size="md">
23
{PROFILE.name}
24
</Heading>
25
-
<Paragraph size="sm">{PROFILE.title}</Paragraph>
26
-
<Paragraph size="sm">{PROFILE.location}</Paragraph>
27
-
<Paragraph size="sm">{PROFILE.experience}</Paragraph>
28
</div>
29
30
-
<div className={styles.section}>
31
-
<Paragraph size="md">{SECTIONS.specializations}</Paragraph>
32
-
<Tags tags={specializations} className="flex-wrap" />
33
-
</div>
34
35
<div className={styles.section}>
36
-
<Paragraph size="md">{SECTIONS.connect}</Paragraph>
37
38
-
<Paragraph className={styles.sectionTitle}>{SECTIONS.connect}</Paragraph>
39
<div className={styles.connectLinks}>
40
{CONNECT_LINKS.map((link) => (
41
<Link key={link.href} href={link.href} target="_blank" rel="noopener noreferrer">
···
1
import { Link } from '@/components/Link/Link';
2
import { Tags } from '@/components/Tags/Tags';
3
+
import { useProtopro } from '@/hooks/atproto';
4
import type { JSX } from 'react';
5
import { Heading } from '../Heading/Heading';
6
import { Paragraph } from '../Paragraph/Paragraph';
···
13
* @returns JSX element with profile photo and summary information
14
*/
15
export default function TLDRProfile(): JSX.Element {
16
+
const { data: profile } = useProtopro();
17
+
18
return (
19
<div className={styles.container}>
20
<div className={styles.innerContainer}>
···
24
<Heading level={3} size="md">
25
{PROFILE.name}
26
</Heading>
27
+
<Paragraph size="base">{PROFILE.location}</Paragraph>
28
+
<Paragraph size="base">{profile?.overview || PROFILE.title}</Paragraph>
29
+
{/* <Paragraph size="base">{PROFILE.experience}</Paragraph> */}
30
</div>
31
32
+
{profile?.skills && profile.skills.length > 0 && (
33
+
<div className={styles.section}>
34
+
<Heading level={4} size="base">
35
+
{SECTIONS.skills}
36
+
</Heading>
37
+
<Tags tags={[...profile.skills].sort().join(', ')} className="flex-wrap" />
38
+
</div>
39
+
)}
40
+
41
+
{profile?.languages && profile.languages.length > 0 && (
42
+
<div className={styles.section}>
43
+
<Heading level={4} size="base">
44
+
{SECTIONS.languages}
45
+
</Heading>
46
+
<Tags
47
+
tags={[...profile.languages]
48
+
.map((lang) => lang.code)
49
+
.sort()
50
+
.join(', ')}
51
+
className="flex-wrap"
52
+
/>
53
+
</div>
54
+
)}
55
56
<div className={styles.section}>
57
+
<Heading level={4} size="base">
58
+
{SECTIONS.connect}
59
+
</Heading>
60
61
<div className={styles.connectLinks}>
62
{CONNECT_LINKS.map((link) => (
63
<Link key={link.href} href={link.href} target="_blank" rel="noopener noreferrer">
+1
-1
src/components/Tag/Tag.styles.ts
+1
-1
src/components/Tag/Tag.styles.ts
+11
-1
src/components/Tag/Tag.tsx
+11
-1
src/components/Tag/Tag.tsx
···
1
import { cn } from '@/lib/utils';
2
import React from 'react';
3
import { Paragraph } from '../Paragraph/Paragraph';
···
5
import { TagProps } from './Tag.types';
6
7
export const Tag: React.FC<TagProps> = ({ label, className = '' }) => {
8
return (
9
-
<div className={cn(tagStyles.base, className)}>
10
<Paragraph size="sm">{label}</Paragraph>
11
</div>
12
);
···
1
+
import { usePageTheme } from '@/components/Layout/Layout';
2
import { cn } from '@/lib/utils';
3
import React from 'react';
4
import { Paragraph } from '../Paragraph/Paragraph';
···
6
import { TagProps } from './Tag.types';
7
8
export const Tag: React.FC<TagProps> = ({ label, className = '' }) => {
9
+
const pageTheme = usePageTheme();
10
+
11
+
// Theme-based tag styling:
12
+
// Accent: white border with semi-transparent white background
13
+
// Default: light backgrounds with dark/light text based on mode
14
+
const themeClasses = pageTheme === 'accent'
15
+
? 'border border-bones-white bg-bones-white-20 text-bones-white'
16
+
: 'bg-bones-black-5 text-bones-black dark:bg-bones-white-10 dark:text-bones-white';
17
+
18
return (
19
+
<div className={cn(tagStyles.base, themeClasses, className)}>
20
<Paragraph size="sm">{label}</Paragraph>
21
</div>
22
);
+8
-7
src/hooks/atproto/fetchPublications.ts
src/hooks/atproto/useLeaflet.ts
+8
-7
src/hooks/atproto/fetchPublications.ts
src/hooks/atproto/useLeaflet.ts
···
1
import { useEffect, useState } from 'react';
2
import { ATPROTO_COLLECTIONS, buildBlobUrl } from '@/config/atproto';
3
import type { ATProtocolDocument, ATProtocolPublication, FetchResult } from '@/types/atproto';
4
-
import { fetchRecords } from './fetchRecords';
5
6
/**
7
* Publication data with resolved metadata
···
28
}
29
30
/**
31
-
* Hook for fetching publications and their associated documents
32
* Automatically resolves publication references in documents
33
*
34
* @returns FetchResult with combined documents and publications data
35
*
36
* @example
37
* ```tsx
38
-
* const { data: posts, loading, error } = fetchPublications();
39
*
40
* if (loading) return <div>Loading...</div>;
41
* if (error) return <div>Error: {error}</div>;
···
48
* ));
49
* ```
50
*/
51
-
export function fetchPublications(): FetchResult<Document[]> {
52
const {
53
data: publicationsData,
54
loading: publicationsLoading,
55
error: publicationsError,
56
-
} = fetchRecords<ATProtocolPublication['value']>(ATPROTO_COLLECTIONS.PUBLICATION);
57
58
const {
59
data: documentsData,
60
loading: documentsLoading,
61
error: documentsError,
62
-
} = fetchRecords<ATProtocolDocument['value']>(ATPROTO_COLLECTIONS.DOCUMENT);
63
64
const [data, setData] = useState<Document[] | null>(null);
65
const [loading, setLoading] = useState(true);
···
119
publishedAt: record.value.publishedAt,
120
articleUrl: `https://${publication.basePath}/${slug}`,
121
publication,
122
-
};
123
})
124
.filter((doc): doc is Document => doc !== null)
125
// Sort by published date, newest first
···
1
import { useEffect, useState } from 'react';
2
import { ATPROTO_COLLECTIONS, buildBlobUrl } from '@/config/atproto';
3
import type { ATProtocolDocument, ATProtocolPublication, FetchResult } from '@/types/atproto';
4
+
import { useRecords } from './useRecords';
5
6
/**
7
* Publication data with resolved metadata
···
28
}
29
30
/**
31
+
* Hook for fetching Leaflet publications and their associated documents
32
+
* Fetches from pub.leaflet.publication and pub.leaflet.document collections
33
* Automatically resolves publication references in documents
34
*
35
* @returns FetchResult with combined documents and publications data
36
*
37
* @example
38
* ```tsx
39
+
* const { data: posts, loading, error } = useLeaflet();
40
*
41
* if (loading) return <div>Loading...</div>;
42
* if (error) return <div>Error: {error}</div>;
···
49
* ));
50
* ```
51
*/
52
+
export function useLeaflet(): FetchResult<Document[]> {
53
const {
54
data: publicationsData,
55
loading: publicationsLoading,
56
error: publicationsError,
57
+
} = useRecords<ATProtocolPublication['value']>(ATPROTO_COLLECTIONS.PUBLICATION);
58
59
const {
60
data: documentsData,
61
loading: documentsLoading,
62
error: documentsError,
63
+
} = useRecords<ATProtocolDocument['value']>(ATPROTO_COLLECTIONS.DOCUMENT);
64
65
const [data, setData] = useState<Document[] | null>(null);
66
const [loading, setLoading] = useState(true);
···
120
publishedAt: record.value.publishedAt,
121
articleUrl: `https://${publication.basePath}/${slug}`,
122
publication,
123
+
} as Document;
124
})
125
.filter((doc): doc is Document => doc !== null)
126
// Sort by published date, newest first
+4
-4
src/hooks/atproto/fetchRecords.ts
src/hooks/atproto/useRecords.ts
+4
-4
src/hooks/atproto/fetchRecords.ts
src/hooks/atproto/useRecords.ts
···
11
*
12
* @example
13
* ```tsx
14
-
* const { data: publications, loading, error } = fetchRecords<PublicationValue>(
15
* 'pub.leaflet.publication'
16
* );
17
* ```
18
*/
19
-
export function fetchRecords<T = unknown>(collection: string, repo?: string): FetchResult<ATProtocolRecord<T>[]> {
20
const [data, setData] = useState<ATProtocolRecord<T>[] | null>(null);
21
const [loading, setLoading] = useState(true);
22
const [error, setError] = useState<string | null>(null);
23
24
useEffect(() => {
25
-
const fetchRecords = async () => {
26
try {
27
setLoading(true);
28
setError(null);
···
44
}
45
};
46
47
-
fetchRecords();
48
}, [collection, repo]);
49
50
return { data, loading, error };
···
11
*
12
* @example
13
* ```tsx
14
+
* const { data: publications, loading, error } = useRecords<PublicationValue>(
15
* 'pub.leaflet.publication'
16
* );
17
* ```
18
*/
19
+
export function useRecords<T = unknown>(collection: string, repo?: string): FetchResult<ATProtocolRecord<T>[]> {
20
const [data, setData] = useState<ATProtocolRecord<T>[] | null>(null);
21
const [loading, setLoading] = useState(true);
22
const [error, setError] = useState<string | null>(null);
23
24
useEffect(() => {
25
+
const loadRecords = async () => {
26
try {
27
setLoading(true);
28
setError(null);
···
44
}
45
};
46
47
+
loadRecords();
48
}, [collection, repo]);
49
50
return { data, loading, error };
+5
-3
src/hooks/atproto/index.ts
+5
-3
src/hooks/atproto/index.ts
+89
src/hooks/atproto/useProtopro.ts
+89
src/hooks/atproto/useProtopro.ts
···
···
1
+
import { useEffect, useState } from 'react';
2
+
import type { FetchResult, JobHistoryEntry, LanguageProficiency, ProfileValue } from '@/types/atproto';
3
+
import { useRecords } from './useRecords';
4
+
5
+
/**
6
+
* Profile data from Protopro collection
7
+
*/
8
+
export interface Profile {
9
+
overview?: string;
10
+
jobHistory: JobHistoryEntry[];
11
+
languages: LanguageProficiency[];
12
+
skills: string[];
13
+
educationHistory: unknown[];
14
+
}
15
+
16
+
/**
17
+
* Hook for fetching Protopro actor profile from AT Protocol
18
+
* Fetches from blue.protopro.actor.profile collection
19
+
* Returns: overview, jobHistory, languages, skills, educationHistory
20
+
*
21
+
* @returns FetchResult with profile data
22
+
*
23
+
* @example
24
+
* ```tsx
25
+
* const { data: profile, loading, error } = useProtopro();
26
+
*
27
+
* if (loading) return <div>Loading...</div>;
28
+
* if (error) return <div>Error: {error}</div>;
29
+
*
30
+
* return (
31
+
* <div>
32
+
* {profile?.jobHistory.map(job => <JobCard key={job.company} job={job} />)}
33
+
* </div>
34
+
* );
35
+
* ```
36
+
*/
37
+
export function useProtopro(): FetchResult<Profile | null> {
38
+
const {
39
+
data: profileData,
40
+
loading: profileLoading,
41
+
error: profileError,
42
+
} = useRecords<ProfileValue>('blue.protopro.actor.profile');
43
+
44
+
const [data, setData] = useState<Profile | null>(null);
45
+
const [loading, setLoading] = useState(true);
46
+
const [error, setError] = useState<string | null>(null);
47
+
48
+
useEffect(() => {
49
+
if (profileLoading) {
50
+
setLoading(true);
51
+
return;
52
+
}
53
+
54
+
if (profileError) {
55
+
setError(profileError);
56
+
setLoading(false);
57
+
return;
58
+
}
59
+
60
+
if (!profileData || profileData.length === 0) {
61
+
setLoading(false);
62
+
return;
63
+
}
64
+
65
+
try {
66
+
// Get the first (and likely only) profile record
67
+
const profileRecord = profileData[0];
68
+
const profileValue = profileRecord.value;
69
+
70
+
const profile: Profile = {
71
+
overview: profileValue.overview,
72
+
jobHistory: profileValue.jobHistory || [],
73
+
languages: profileValue.languages || [],
74
+
skills: profileValue.skills || [],
75
+
educationHistory: profileValue.educationHistory || [],
76
+
};
77
+
78
+
setData(profile);
79
+
setError(null);
80
+
} catch (err) {
81
+
console.error('Error processing profile data:', err);
82
+
setError(err instanceof Error ? err.message : 'An error occurred');
83
+
} finally {
84
+
setLoading(false);
85
+
}
86
+
}, [profileData, profileLoading, profileError]);
87
+
88
+
return { data, loading, error };
89
+
}
+26
-8
src/index.css
+26
-8
src/index.css
···
58
line-height: 1.62;
59
}
60
61
-
/* md: Body copy, paragraphs (16px → 18px) */
62
.type-md {
63
@apply fluid-preset-md;
64
line-height: 1.38;
···
88
*/
89
90
.heading-display {
91
-
@apply type-2xl font-semibold font-sans;
92
}
93
94
.heading-2xl {
···
107
@apply type-md font-extrabold font-sans;
108
}
109
110
.heading-sm {
111
-
@apply type-sm font-semibold font-sans;
112
}
113
114
/**
···
132
@apply type-md font-normal font-sans;
133
}
134
135
.paragraph-sm {
136
@apply type-sm font-normal font-sans;
137
}
···
141
* Uses Tailwind's native font-serif, italic
142
*/
143
144
-
.quote-xl {
145
@apply type-2xl font-normal font-sans italic;
146
}
147
148
.quote-lg {
149
-
@apply type-xl font-normal font-serif italic;
150
}
151
152
.quote-md {
153
-
@apply type-lg font-normal font-serif italic;
154
}
155
156
/**
···
159
*/
160
161
.code-inline {
162
-
@apply type-sm font-normal font-mono;
163
}
164
165
.code-block {
166
-
@apply type-sm font-normal font-mono;
167
line-height: 1.75;
168
}
169
···
58
line-height: 1.62;
59
}
60
61
+
/* base: Default text (16px → 32px) */
62
+
.type-base {
63
+
@apply fluid-preset-base;
64
+
line-height: 1.5;
65
+
}
66
+
67
+
/* md: Body copy, paragraphs (24px → 40px) */
68
.type-md {
69
@apply fluid-preset-md;
70
line-height: 1.38;
···
94
*/
95
96
.heading-display {
97
+
@apply type-2xl font-bold font-sans;
98
}
99
100
.heading-2xl {
···
113
@apply type-md font-extrabold font-sans;
114
}
115
116
+
.heading-base {
117
+
@apply type-base font-black font-sans;
118
+
}
119
+
120
.heading-sm {
121
+
@apply type-sm font-black font-sans;
122
}
123
124
/**
···
142
@apply type-md font-normal font-sans;
143
}
144
145
+
.paragraph-base {
146
+
@apply type-base font-normal font-sans;
147
+
}
148
+
149
.paragraph-sm {
150
@apply type-sm font-normal font-sans;
151
}
···
155
* Uses Tailwind's native font-serif, italic
156
*/
157
158
+
.quote-2xl {
159
@apply type-2xl font-normal font-sans italic;
160
+
}
161
+
162
+
.quote-xl {
163
+
@apply type-xl font-normal font-sans italic;
164
}
165
166
.quote-lg {
167
+
@apply type-lg font-normal font-serif italic;
168
}
169
170
.quote-md {
171
+
@apply type-md font-normal font-serif italic;
172
}
173
174
/**
···
177
*/
178
179
.code-inline {
180
+
@apply type-base font-normal font-mono;
181
}
182
183
.code-block {
184
+
@apply type-base font-normal font-mono;
185
line-height: 1.75;
186
}
187
+2
-2
src/pages/AboutPage.tsx
+2
-2
src/pages/AboutPage.tsx
···
164
Recent Work
165
</Heading>
166
167
-
<div className="grid grid-cols-1 border-2 border-bones-white-30">
168
{latestJobs.map((job, index) => {
169
const startYear = new Date(job.startDate).getFullYear();
170
const endYear = job.endDate ? new Date(job.endDate).getFullYear() : 'Present';
···
200
<Paragraph size="lg">My approach is straightforward and outcome-focused.</Paragraph>
201
202
<UnorderedList bullet="disc">
203
-
<ListItem size="lg">I start by understanding users and the problems they face</ListItem>
204
<ListItem>I design and prototype quickly to validate ideas early</ListItem>
205
<ListItem>I measure results and refine based on evidence</ListItem>
206
<ListItem>I document decisions to help teams move with clarity</ListItem>
···
164
Recent Work
165
</Heading>
166
167
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
168
{latestJobs.map((job, index) => {
169
const startYear = new Date(job.startDate).getFullYear();
170
const endYear = job.endDate ? new Date(job.endDate).getFullYear() : 'Present';
···
200
<Paragraph size="lg">My approach is straightforward and outcome-focused.</Paragraph>
201
202
<UnorderedList bullet="disc">
203
+
<ListItem>I start by understanding users and the problems they face</ListItem>
204
<ListItem>I design and prototype quickly to validate ideas early</ListItem>
205
<ListItem>I measure results and refine based on evidence</ListItem>
206
<ListItem>I document decisions to help teams move with clarity</ListItem>
+2
-5
src/pages/HomePage.tsx
+2
-5
src/pages/HomePage.tsx
···
63
</Paragraph>
64
65
<Paragraph size="2xl">
66
-
I{' '}
67
-
<Link href="https://www.linkedin.com/in/barrymprendergast/details/experience/" target="_blank">
68
-
work
69
-
</Link>{' '}
70
-
with nonprofits and startups to ease their growing pains, and to market faster.
71
</Paragraph>
72
73
<Paragraph size="2xl">
+130
src/pages/WorkPage.tsx
+130
src/pages/WorkPage.tsx
···
···
1
+
import { CardRole } from '@/components/CardRole/CardRole';
2
+
import { Divider } from '@/components/Divider/Divider';
3
+
import { Heading } from '@/components/Heading/Heading';
4
+
import { Aside, Layout, Main } from '@/components/Layout/Layout';
5
+
import { Link } from '@/components/Link/Link';
6
+
import { Paragraph } from '@/components/Paragraph/Paragraph';
7
+
import Section from '@/components/Section/Section';
8
+
import TLDRProfile from '@/components/TLDRProfile/TLDRProfile';
9
+
import { useProtopro } from '@/hooks/atproto';
10
+
import type { JSX } from 'react';
11
+
import { Helmet } from 'react-helmet-async';
12
+
13
+
/**
14
+
* Work page component - displays CV/work history from AT Protocol PDS
15
+
*
16
+
* @returns JSX element with work page content
17
+
*/
18
+
export default function WorkPage(): JSX.Element {
19
+
const { data: profile, loading, error } = useProtopro();
20
+
21
+
// Split jobs into current and past
22
+
const currentJobs = profile?.jobHistory.filter((job) => !job.endDate) || [];
23
+
const pastJobs =
24
+
profile?.jobHistory
25
+
.filter((job) => job.endDate)
26
+
.sort((a, b) => {
27
+
// Sort by end date, newest first
28
+
const dateA = a.endDate ? new Date(a.endDate).getTime() : 0;
29
+
const dateB = b.endDate ? new Date(b.endDate).getTime() : 0;
30
+
return dateB - dateA;
31
+
}) || [];
32
+
33
+
const jsonLd = {
34
+
'@context': 'https://schema.org',
35
+
'@type': 'ProfilePage',
36
+
mainEntity: {
37
+
'@type': 'Person',
38
+
name: 'Barry Prendergast',
39
+
jobTitle: currentJobs[0]?.position || 'Product Designer',
40
+
description: 'Independent product designer and strategist',
41
+
url: 'https://renderg.host/work',
42
+
},
43
+
};
44
+
45
+
return (
46
+
<>
47
+
<Helmet>
48
+
<title>Work | Barry Prendergast</title>
49
+
<meta
50
+
name="description"
51
+
content="Independent product designer helping organisations deliver better products through clear thinking, practical design, and meaningful collaboration."
52
+
/>
53
+
<script type="application/ld+json" dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }} />
54
+
</Helmet>
55
+
56
+
<Layout theme="default">
57
+
<Main>
58
+
<div className="flex flex-col gap-16">
59
+
{/* Heading */}
60
+
<Section>
61
+
<Heading level={2} size="base">
62
+
work / <Link href="/">renderg.host</Link>
63
+
</Heading>
64
+
</Section>
65
+
66
+
{/* Loading/Error States */}
67
+
{loading && (
68
+
<Section>
69
+
<Paragraph size="2xl">Loading profile...</Paragraph>
70
+
</Section>
71
+
)}
72
+
73
+
{error && (
74
+
<Section>
75
+
<Paragraph size="2xl">Error loading profile: {error}</Paragraph>
76
+
</Section>
77
+
)}
78
+
79
+
{/* Current Work Section */}
80
+
{!loading && !error && currentJobs.length > 0 && (
81
+
<Section>
82
+
<Heading level={2} size="lg">
83
+
Current roles
84
+
</Heading>
85
+
86
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
87
+
{currentJobs.map((job, index) => (
88
+
<>
89
+
<CardRole key={`current-${job.company}-${index}`} role={job} />
90
+
{index < currentJobs.length - 1 && <Divider />}
91
+
</>
92
+
))}
93
+
</div>
94
+
</Section>
95
+
)}
96
+
97
+
{/* Past Work Section */}
98
+
{!loading && !error && pastJobs.length > 0 && (
99
+
<Section>
100
+
<Heading level={2} size="lg">
101
+
Previous roles
102
+
</Heading>
103
+
104
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
105
+
{pastJobs.map((job, index) => (
106
+
<>
107
+
<CardRole key={`past-${job.company}-${index}`} role={job} />
108
+
{index < pastJobs.length - 1 && <Divider />}
109
+
</>
110
+
))}
111
+
</div>
112
+
</Section>
113
+
)}
114
+
115
+
{/* Exit */}
116
+
<Section>
117
+
<Paragraph size="md">
118
+
Return to <Link href="/">renderg.host</Link>
119
+
</Paragraph>
120
+
</Section>
121
+
</div>
122
+
</Main>
123
+
124
+
<Aside>
125
+
<TLDRProfile />
126
+
</Aside>
127
+
</Layout>
128
+
</>
129
+
);
130
+
}
src/pages/WorksPage.tsx
src/pages/PortfolioPage.tsx
src/pages/WorksPage.tsx
src/pages/PortfolioPage.tsx
+5
-8
src/pages/WritingPage.tsx
+5
-8
src/pages/WritingPage.tsx
···
5
import { Link } from '@/components/Link/Link';
6
import { Paragraph } from '@/components/Paragraph/Paragraph';
7
import TLDRProfile from '@/components/TLDRProfile/TLDRProfile';
8
-
import { fetchPublications } from '@/hooks/atproto';
9
import type { JSX } from 'react';
10
import { Helmet } from 'react-helmet-async';
11
···
16
*/
17
18
export default function WritingPage(): JSX.Element {
19
-
const { data: documents, loading, error } = fetchPublications();
20
21
const jsonLd = {
22
'@context': 'https://schema.org',
···
45
<Layout theme="default">
46
<Main>
47
<div className="flex flex-col gap-12">
48
-
<Paragraph size="md">
49
-
<Link href="/">←Home </Link>
50
-
</Paragraph>
51
-
<Heading level={1} size="md">
52
-
My Writing
53
</Heading>
54
55
<Paragraph size="lg">Thoughts on design strategy, product design, and building better products.</Paragraph>
···
63
)}
64
65
{!loading && !error && documents && documents.length > 0 && (
66
-
<div className="grid grid-cols-1 border-2 border-bones-white-30">
67
{documents.map((doc, index) => (
68
<>
69
<CardArticle
···
5
import { Link } from '@/components/Link/Link';
6
import { Paragraph } from '@/components/Paragraph/Paragraph';
7
import TLDRProfile from '@/components/TLDRProfile/TLDRProfile';
8
+
import { useLeaflet } from '@/hooks/atproto';
9
import type { JSX } from 'react';
10
import { Helmet } from 'react-helmet-async';
11
···
16
*/
17
18
export default function WritingPage(): JSX.Element {
19
+
const { data: documents, loading, error } = useLeaflet();
20
21
const jsonLd = {
22
'@context': 'https://schema.org',
···
45
<Layout theme="default">
46
<Main>
47
<div className="flex flex-col gap-12">
48
+
<Heading level={2} size="md">
49
+
writing / <Link href="/">renderg.host</Link>
50
</Heading>
51
52
<Paragraph size="lg">Thoughts on design strategy, product design, and building better products.</Paragraph>
···
60
)}
61
62
{!loading && !error && documents && documents.length > 0 && (
63
+
<div className="grid grid-cols-1 border-2 border-bones-black-20 dark:border-bones-white-20">
64
{documents.map((doc, index) => (
65
<>
66
<CardArticle
+43
src/types/atproto/defineBase.ts
+43
src/types/atproto/defineBase.ts
···
···
1
+
/**
2
+
* Base AT Protocol type definitions
3
+
* Generic types used across all AT Protocol collections
4
+
*/
5
+
6
+
/**
7
+
* AT Protocol Blob reference
8
+
* Used for binary data like images
9
+
*/
10
+
export interface ATProtocolBlob {
11
+
$type: 'blob';
12
+
ref: {
13
+
$link: string;
14
+
};
15
+
mimeType: string;
16
+
size: number;
17
+
}
18
+
19
+
/**
20
+
* Base AT Protocol record structure
21
+
*/
22
+
export interface ATProtocolRecord<T = unknown> {
23
+
uri: string;
24
+
cid: string;
25
+
value: T;
26
+
}
27
+
28
+
/**
29
+
* Response from listRecords XRPC endpoint
30
+
*/
31
+
export interface ListRecordsResponse<T = unknown> {
32
+
records: ATProtocolRecord<T>[];
33
+
cursor?: string;
34
+
}
35
+
36
+
/**
37
+
* Generic fetch result with loading and error states
38
+
*/
39
+
export interface FetchResult<T> {
40
+
data: T | null;
41
+
loading: boolean;
42
+
error: string | null;
43
+
}
+38
src/types/atproto/defineLeaflet.ts
+38
src/types/atproto/defineLeaflet.ts
···
···
1
+
/**
2
+
* Leaflet collection type definitions
3
+
* Types for pub.leaflet.publication and pub.leaflet.document
4
+
*/
5
+
6
+
import type { ATProtocolBlob, ATProtocolRecord } from './defineBase';
7
+
8
+
/**
9
+
* Publication record value structure
10
+
*/
11
+
export interface PublicationValue {
12
+
name: string;
13
+
base_path: string;
14
+
icon?: ATProtocolBlob;
15
+
description?: string;
16
+
$type: string;
17
+
}
18
+
19
+
/**
20
+
* Document record value structure
21
+
*/
22
+
export interface DocumentValue {
23
+
title: string;
24
+
description?: string;
25
+
publishedAt: string;
26
+
publication: string; // URI reference to the publication
27
+
$type: string;
28
+
}
29
+
30
+
/**
31
+
* Full Publication record
32
+
*/
33
+
export type ATProtocolPublication = ATProtocolRecord<PublicationValue>;
34
+
35
+
/**
36
+
* Full Document record
37
+
*/
38
+
export type ATProtocolDocument = ATProtocolRecord<DocumentValue>;
+45
src/types/atproto/defineProtopro.ts
+45
src/types/atproto/defineProtopro.ts
···
···
1
+
/**
2
+
* Protopro collection type definitions
3
+
* Types for blue.protopro.actor.profile
4
+
*/
5
+
6
+
import type { ATProtocolRecord } from './defineBase';
7
+
8
+
/**
9
+
* Language proficiency structure
10
+
*/
11
+
export interface LanguageProficiency {
12
+
code: string;
13
+
level: number;
14
+
}
15
+
16
+
/**
17
+
* Job history entry structure
18
+
*/
19
+
export interface JobHistoryEntry {
20
+
company: string;
21
+
position: string;
22
+
startDate: string;
23
+
endDate?: string;
24
+
description?: string;
25
+
}
26
+
27
+
/**
28
+
* Actor Profile record value structure
29
+
*/
30
+
export interface ProfileValue {
31
+
name: string;
32
+
skills: string[];
33
+
overview?: string;
34
+
languages?: LanguageProficiency[];
35
+
jobHistory?: JobHistoryEntry[];
36
+
socialLinks?: string[];
37
+
educationHistory?: unknown[];
38
+
updatedAt?: string;
39
+
$type: string;
40
+
}
41
+
42
+
/**
43
+
* Full Profile record
44
+
*/
45
+
export type ATProtocolProfile = ATProtocolRecord<ProfileValue>;
-66
src/types/atproto/defineRecords.ts
-66
src/types/atproto/defineRecords.ts
···
1
-
/**
2
-
* AT Protocol record type definitions
3
-
* Type-safe interfaces for AT Protocol data structures
4
-
*/
5
-
6
-
/**
7
-
* AT Protocol Blob reference
8
-
* Used for binary data like images
9
-
*/
10
-
export interface ATProtocolBlob {
11
-
$type: 'blob';
12
-
ref: {
13
-
$link: string;
14
-
};
15
-
mimeType: string;
16
-
size: number;
17
-
}
18
-
19
-
/**
20
-
* Base AT Protocol record structure
21
-
*/
22
-
export interface ATProtocolRecord<T = unknown> {
23
-
uri: string;
24
-
cid: string;
25
-
value: T;
26
-
}
27
-
28
-
/**
29
-
* Publication record value structure
30
-
*/
31
-
export interface PublicationValue {
32
-
name: string;
33
-
base_path: string;
34
-
icon?: ATProtocolBlob;
35
-
description?: string;
36
-
$type: string;
37
-
}
38
-
39
-
/**
40
-
* Document record value structure
41
-
*/
42
-
export interface DocumentValue {
43
-
title: string;
44
-
description?: string;
45
-
publishedAt: string;
46
-
publication: string; // URI reference to the publication
47
-
$type: string;
48
-
}
49
-
50
-
/**
51
-
* Full Publication record
52
-
*/
53
-
export type ATProtocolPublication = ATProtocolRecord<PublicationValue>;
54
-
55
-
/**
56
-
* Full Document record
57
-
*/
58
-
export type ATProtocolDocument = ATProtocolRecord<DocumentValue>;
59
-
60
-
/**
61
-
* Response from listRecords XRPC endpoint
62
-
*/
63
-
export interface ListRecordsResponse<T = unknown> {
64
-
records: ATProtocolRecord<T>[];
65
-
cursor?: string;
66
-
}
···
-12
src/types/atproto/defineResults.ts
-12
src/types/atproto/defineResults.ts
+12
-10
src/types/atproto/index.ts
+12
-10
src/types/atproto/index.ts
···
2
* AT Protocol type exports
3
*/
4
5
-
export type {
6
-
ATProtocolBlob,
7
-
ATProtocolDocument,
8
-
ATProtocolPublication,
9
-
ATProtocolRecord,
10
-
DocumentValue,
11
-
ListRecordsResponse,
12
-
PublicationValue,
13
-
} from './defineRecords';
14
15
-
export type { FetchResult } from './defineResults';
···
2
* AT Protocol type exports
3
*/
4
5
+
// Base types
6
+
export type { ATProtocolBlob, ATProtocolRecord, FetchResult, ListRecordsResponse } from './defineBase';
7
8
+
// Leaflet types
9
+
export type { ATProtocolDocument, ATProtocolPublication, DocumentValue, PublicationValue } from './defineLeaflet';
10
+
11
+
// Protopro types
12
+
export type {
13
+
ATProtocolProfile,
14
+
JobHistoryEntry,
15
+
LanguageProficiency,
16
+
ProfileValue,
17
+
} from './defineProtopro';
+5
-1
tsconfig.json
+5
-1
tsconfig.json