Skip to content

Commit ddf9924

Browse files
committed
migrate: Add initial version of Learn:API page
1 parent a0b3346 commit ddf9924

File tree

8 files changed

+614
-0
lines changed

8 files changed

+614
-0
lines changed

app/src/Router.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { PolicyCreationFlow } from './flows/policyCreationFlow';
66
import { PopulationCreationFlow } from './flows/populationCreationFlow';
77
import { ReportCreationFlow } from './flows/reportCreationFlow';
88
import { SimulationCreationFlow } from './flows/simulationCreationFlow';
9+
import APIPage from './pages/API.page';
910
import DonatePage from './pages/Donate.page';
1011
import PoliciesPage from './pages/Policies.page';
1112
import PolicyDesign1Page from './pages/policy-designs/PolicyDesign1.page';
@@ -153,6 +154,21 @@ const router = createBrowserRouter(
153154
},
154155
],
155156
},
157+
// Static pages that need metadata - use MetadataLazyLoader + StaticLayout
158+
{
159+
element: <MetadataLazyLoader />,
160+
children: [
161+
{
162+
element: <StaticLayout />,
163+
children: [
164+
{
165+
path: 'api',
166+
element: <APIPage />,
167+
},
168+
],
169+
},
170+
],
171+
},
156172
// Routes that don't need metadata at all (no guard)
157173
{
158174
element: <Layout />,
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
import { Badge, Card, Stack, Text } from '@mantine/core';
2+
import { colors, spacing, typography } from '@/designTokens';
3+
4+
interface ParameterMetadata {
5+
type: 'parameter';
6+
parameter: string;
7+
label?: string;
8+
description?: string;
9+
unit?: string;
10+
period?: string | null;
11+
economy?: boolean;
12+
household?: boolean;
13+
values?: Record<string, number>;
14+
}
15+
16+
interface VariableMetadata {
17+
name: string;
18+
label?: string;
19+
description?: string;
20+
entity?: string;
21+
definitionPeriod?: string;
22+
unit?: string;
23+
category?: string;
24+
defaultValue?: number;
25+
isInputVariable?: boolean;
26+
valueType?: string;
27+
}
28+
29+
interface APIMetadataCardProps {
30+
metadata: ParameterMetadata | VariableMetadata;
31+
onClick?: () => void;
32+
}
33+
34+
export default function APIMetadataCard({ metadata, onClick }: APIMetadataCardProps) {
35+
const isParameter = 'type' in metadata && metadata.type === 'parameter';
36+
const paramData = isParameter ? (metadata as ParameterMetadata) : null;
37+
const varData = !isParameter ? (metadata as VariableMetadata) : null;
38+
39+
const displayLabel = paramData?.label || varData?.label || varData?.name || '';
40+
const description = metadata.description || '';
41+
const pythonName = paramData?.parameter || varData?.name || '';
42+
43+
return (
44+
<Card
45+
shadow="xs"
46+
padding={spacing.md}
47+
radius="md"
48+
onClick={onClick}
49+
style={{
50+
cursor: onClick ? 'pointer' : 'default',
51+
transition: 'box-shadow 0.2s ease',
52+
backgroundColor: colors.white,
53+
border: `1px solid ${colors.gray[200]}`,
54+
height: '100%',
55+
}}
56+
styles={{
57+
root: {
58+
'&:hover': {
59+
boxShadow: onClick ? '0 4px 12px rgba(0,0,0,0.1)' : undefined,
60+
},
61+
},
62+
}}
63+
>
64+
<Stack gap={spacing.sm}>
65+
<Badge color={isParameter ? 'green' : 'red'} size="sm">
66+
{isParameter ? 'Parameter' : 'Variable'}
67+
</Badge>
68+
69+
<Text fw={typography.fontWeight.semibold} style={{ fontSize: typography.fontSize.base }}>
70+
{displayLabel}
71+
</Text>
72+
73+
{description && (
74+
<Text
75+
style={{
76+
fontSize: typography.fontSize.sm,
77+
color: colors.text.secondary,
78+
lineHeight: 1.5,
79+
}}
80+
>
81+
{description}
82+
</Text>
83+
)}
84+
85+
{varData?.entity && (
86+
<Text style={{ fontSize: typography.fontSize.xs, color: colors.text.tertiary }}>
87+
<strong>Entity:</strong> {varData.entity}
88+
</Text>
89+
)}
90+
91+
{(paramData?.period || varData?.definitionPeriod) && (
92+
<Text style={{ fontSize: typography.fontSize.xs, color: colors.text.tertiary }}>
93+
<strong>Period:</strong> {paramData?.period || varData?.definitionPeriod}
94+
</Text>
95+
)}
96+
97+
{metadata.unit && (
98+
<Text style={{ fontSize: typography.fontSize.xs, color: colors.text.tertiary }}>
99+
<strong>Unit:</strong> {metadata.unit}
100+
</Text>
101+
)}
102+
103+
<Text
104+
style={{
105+
fontSize: typography.fontSize.xs,
106+
color: colors.text.tertiary,
107+
wordBreak: 'break-all',
108+
fontFamily: 'monospace',
109+
}}
110+
>
111+
<strong>Python name:</strong> {pythonName}
112+
</Text>
113+
</Stack>
114+
</Card>
115+
);
116+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { Box } from '@mantine/core';
2+
3+
interface APIPlaygroundProps {
4+
countryId: string;
5+
}
6+
7+
export default function APIPlayground({ countryId }: APIPlaygroundProps) {
8+
const streamlitUrl = `https://policyengine-api-light.streamlit.app/?embedded=true&country_id=${countryId}`;
9+
10+
return (
11+
<Box
12+
component="iframe"
13+
src={streamlitUrl}
14+
style={{
15+
width: '100%',
16+
height: '600px',
17+
border: 'none',
18+
borderRadius: '8px',
19+
}}
20+
title="API Playground"
21+
/>
22+
);
23+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { IconCheck, IconCopy } from '@tabler/icons-react';
2+
import { ActionIcon, Box, Code, CopyButton, Stack, Text } from '@mantine/core';
3+
import { colors, spacing, typography } from '@/designTokens';
4+
5+
interface CodeBlockProps {
6+
code: string;
7+
language?: string;
8+
title?: string;
9+
}
10+
11+
export default function CodeBlock({ code, language: _language, title }: CodeBlockProps) {
12+
return (
13+
<Stack gap={spacing.xs}>
14+
{title && (
15+
<Text
16+
fw={typography.fontWeight.semibold}
17+
style={{
18+
fontSize: typography.fontSize.sm,
19+
color: colors.text.secondary,
20+
}}
21+
>
22+
{title}
23+
</Text>
24+
)}
25+
<Box pos="relative">
26+
<Code
27+
block
28+
style={{
29+
fontSize: typography.fontSize.sm,
30+
padding: spacing.md,
31+
backgroundColor: colors.gray[50],
32+
border: `1px solid ${colors.gray[200]}`,
33+
borderRadius: '8px',
34+
overflowX: 'auto',
35+
maxHeight: '400px',
36+
overflowY: 'auto',
37+
}}
38+
>
39+
{code}
40+
</Code>
41+
<Box
42+
style={{
43+
position: 'absolute',
44+
top: spacing.sm,
45+
right: spacing.sm,
46+
}}
47+
>
48+
<CopyButton value={code} timeout={2000}>
49+
{({ copied, copy }) => (
50+
<ActionIcon
51+
color={copied ? 'teal' : 'gray'}
52+
variant="filled"
53+
onClick={copy}
54+
size="sm"
55+
>
56+
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
57+
</ActionIcon>
58+
)}
59+
</CopyButton>
60+
</Box>
61+
</Box>
62+
</Stack>
63+
);
64+
}

app/src/components/shared/static/ContentSection.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,15 @@ export interface ContentSectionProps {
77
variant?: 'primary' | 'secondary' | 'accent';
88
children: ReactNode;
99
centerTitle?: boolean;
10+
id?: string;
1011
}
1112

1213
export default function ContentSection({
1314
title,
1415
variant = 'primary',
1516
children,
1617
centerTitle = false,
18+
id,
1719
}: ContentSectionProps) {
1820
const backgrounds = {
1921
primary: colors.white,
@@ -29,6 +31,7 @@ export default function ContentSection({
2931

3032
return (
3133
<Box
34+
id={id}
3235
py={spacing['4xl']}
3336
style={{
3437
backgroundColor: backgrounds[variant],

0 commit comments

Comments
 (0)