Skip to content

Commit 9b96161

Browse files
authored
docs api query builder and json updates (#72)
* updates fixed biome complexity issue with jsonnode and made it look nicer * api base url in actions should refer the env var * fix: light mode json node * update: design changes. rebasing to staging next - updates query types grid design - added query types dialog
1 parent 27fbd2a commit 9b96161

File tree

6 files changed

+769
-285
lines changed

6 files changed

+769
-285
lines changed

apps/docs/app/(home)/api/actions.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,34 @@
11
'use server';
22

3+
import type { QueryBuilderMeta } from '@databuddy/shared';
4+
35
interface QueryConfig {
46
allowedFilters: string[];
57
customizable: boolean;
68
defaultLimit: number;
79
}
810

11+
export interface QueryConfigWithMeta extends QueryConfig {
12+
meta?: QueryBuilderMeta;
13+
}
14+
915
interface QueryTypesResponse {
1016
success: boolean;
1117
types: string[];
12-
configs: Record<string, QueryConfig>;
18+
configs: Record<string, QueryConfigWithMeta>;
1319
}
1420

15-
export async function getQueryTypes(): Promise<QueryTypesResponse> {
21+
const API_BASE_URL =
22+
process.env.NEXT_PUBLIC_API_URL || 'https://api.databuddy.cc';
23+
24+
export async function getQueryTypes(
25+
includeMeta = false
26+
): Promise<QueryTypesResponse> {
1627
try {
17-
const response = await fetch('https://api.databuddy.cc/v1/query/types', {
28+
const url = new URL(`${API_BASE_URL}/v1/query/types`);
29+
if (includeMeta) url.searchParams.set('include_meta', 'true');
30+
31+
const response = await fetch(url.toString(), {
1832
method: 'GET',
1933
headers: {
2034
'Content-Type': 'application/json',
@@ -27,7 +41,7 @@ export async function getQueryTypes(): Promise<QueryTypesResponse> {
2741
throw new Error(`API responded with status: ${response.status}`);
2842
}
2943

30-
const data = await response.json();
44+
const data = (await response.json()) as QueryTypesResponse;
3145
return data;
3246
} catch (error) {
3347
console.error('Failed to fetch query types:', error);
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
'use client';
2+
3+
import { CaretDownIcon, CaretRightIcon } from '@phosphor-icons/react';
4+
import { useState } from 'react';
5+
6+
export interface JsonNodeProps {
7+
data: unknown;
8+
name?: string;
9+
level?: number;
10+
}
11+
12+
function getValueColor(value: unknown) {
13+
if (value === null) {
14+
return 'text-muted-foreground';
15+
}
16+
if (typeof value === 'string') {
17+
return 'text-emerald-500 dark:text-emerald-300';
18+
}
19+
if (typeof value === 'number' || typeof value === 'boolean') {
20+
return 'text-amber-500 dark:text-amber-300';
21+
}
22+
return 'text-foreground/90';
23+
}
24+
25+
function formatValue(value: unknown) {
26+
if (value === null) {
27+
return 'null';
28+
}
29+
if (typeof value === 'string') {
30+
return `"${value}"`;
31+
}
32+
return String(value);
33+
}
34+
35+
function PrimitiveNode({
36+
value,
37+
name,
38+
level,
39+
}: {
40+
value: unknown;
41+
name?: string;
42+
level: number;
43+
}) {
44+
const indent = level * 12;
45+
return (
46+
<div
47+
className="flex items-center rounded px-2 py-1 transition-colors hover:bg-muted/20"
48+
style={{ paddingLeft: indent }}
49+
>
50+
{name && <span className="mr-2 text-primary">{name}:</span>}
51+
<span className={getValueColor(value)}>{formatValue(value)}</span>
52+
</div>
53+
);
54+
}
55+
56+
function ArrayNode({
57+
data,
58+
name,
59+
level,
60+
}: {
61+
data: unknown[];
62+
name?: string;
63+
level: number;
64+
}) {
65+
const [isExpanded, setIsExpanded] = useState(true);
66+
const indent = level * 12;
67+
if (data.length === 0) {
68+
return <PrimitiveNode level={level} name={name} value="[]" />;
69+
}
70+
return (
71+
<div>
72+
<button
73+
aria-expanded={isExpanded}
74+
className="flex w-full items-center rounded px-2 py-1 text-left transition-colors hover:bg-muted/20"
75+
onClick={() => setIsExpanded(!isExpanded)}
76+
style={{ paddingLeft: indent }}
77+
type="button"
78+
>
79+
{isExpanded ? (
80+
<CaretDownIcon className="mr-1 h-4 w-4 text-muted-foreground" />
81+
) : (
82+
<CaretRightIcon className="mr-1 h-4 w-4 text-muted-foreground" />
83+
)}
84+
{name && <span className="mr-2 text-primary">{name}:</span>}
85+
<span className="font-semibold text-foreground/80">[</span>
86+
</button>
87+
{isExpanded && (
88+
<>
89+
{data.map((item, index) => (
90+
<JsonNode
91+
data={item}
92+
key={`${name || 'root'}-${index}`}
93+
level={level + 1}
94+
/>
95+
))}
96+
<div
97+
className="flex items-center py-1"
98+
style={{ paddingLeft: indent }}
99+
>
100+
<span className="font-semibold text-foreground/80">]</span>
101+
</div>
102+
</>
103+
)}
104+
{!isExpanded && (
105+
<div className="flex items-center py-1" style={{ paddingLeft: indent }}>
106+
<span className="font-semibold text-foreground/80">]</span>
107+
</div>
108+
)}
109+
</div>
110+
);
111+
}
112+
113+
function ObjectNode({
114+
data,
115+
name,
116+
level,
117+
}: {
118+
data: Record<string, unknown>;
119+
name?: string;
120+
level: number;
121+
}) {
122+
const [isExpanded, setIsExpanded] = useState(true);
123+
const indent = level * 12;
124+
const keys = Object.keys(data);
125+
if (keys.length === 0) {
126+
return <PrimitiveNode level={level} name={name} value="{}" />;
127+
}
128+
return (
129+
<div>
130+
<button
131+
aria-expanded={isExpanded}
132+
className="flex w-full items-center rounded px-2 py-1 text-left transition-colors hover:bg-muted/20"
133+
onClick={() => setIsExpanded(!isExpanded)}
134+
style={{ paddingLeft: indent }}
135+
type="button"
136+
>
137+
{isExpanded ? (
138+
<CaretDownIcon className="mr-1 h-4 w-4 text-muted-foreground" />
139+
) : (
140+
<CaretRightIcon className="mr-1 h-4 w-4 text-muted-foreground" />
141+
)}
142+
{name && <span className="mr-2 text-primary">{name}:</span>}
143+
<span className="font-semibold text-foreground/80">{'{'}</span>
144+
</button>
145+
{isExpanded && (
146+
<>
147+
{keys.map((key) => (
148+
<JsonNode data={data[key]} key={key} level={level + 1} name={key} />
149+
))}
150+
<div
151+
className="flex items-center py-1"
152+
style={{ paddingLeft: indent }}
153+
>
154+
<span className="font-semibold text-foreground/80">{'}'}</span>
155+
</div>
156+
</>
157+
)}
158+
{!isExpanded && (
159+
<div className="flex items-center py-1" style={{ paddingLeft: indent }}>
160+
<span className="font-semibold text-foreground/80">{'}'}</span>
161+
</div>
162+
)}
163+
</div>
164+
);
165+
}
166+
167+
export function JsonNode({ data, name, level = 0 }: JsonNodeProps) {
168+
if (
169+
data === null ||
170+
typeof data === 'string' ||
171+
typeof data === 'number' ||
172+
typeof data === 'boolean'
173+
) {
174+
return <PrimitiveNode level={level} name={name} value={data} />;
175+
}
176+
if (Array.isArray(data)) {
177+
return <ArrayNode data={data} level={level} name={name} />;
178+
}
179+
if (typeof data === 'object') {
180+
return (
181+
<ObjectNode
182+
data={data as Record<string, unknown>}
183+
level={level}
184+
name={name}
185+
/>
186+
);
187+
}
188+
return null;
189+
}

apps/docs/app/(home)/api/page.tsx

Lines changed: 7 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
} from '@/components/ui/card';
99
import { getQueryTypes } from './actions';
1010
import { QueryDemo } from './query-demo';
11+
import { QueryTypesGrid } from './query-types-grid';
1112

1213
export default async function ApiPlaygroundPage() {
1314
const queryTypesData = await getQueryTypes();
@@ -58,45 +59,12 @@ export default async function ApiPlaygroundPage() {
5859
</div>
5960
</div>
6061
) : (
61-
<div className="grid gap-3 md:grid-cols-2 lg:grid-cols-3">
62-
{queryTypesData.types.sort().map((type) => (
63-
<div className="rounded border p-4" key={type}>
64-
<div className="space-y-3">
65-
<div className="flex items-start justify-between">
66-
<code className="font-medium font-mono text-sm">
67-
{type}
68-
</code>
69-
{queryTypesData.configs[type]?.customizable && (
70-
<span className="rounded bg-blue-100 px-2 py-1 text-blue-800 text-xs">
71-
Customizable
72-
</span>
73-
)}
74-
</div>
75-
76-
{queryTypesData.configs[type] && (
77-
<div className="space-y-2">
78-
{queryTypesData.configs[type].defaultLimit && (
79-
<div className="text-muted-foreground text-xs">
80-
Default limit:{' '}
81-
{queryTypesData.configs[type].defaultLimit}
82-
</div>
83-
)}
84-
85-
{queryTypesData.configs[type].allowedFilters
86-
?.length > 0 && (
87-
<div className="text-muted-foreground text-xs">
88-
<span className="font-medium">Filters:</span>{' '}
89-
{queryTypesData.configs[
90-
type
91-
].allowedFilters.join(', ')}
92-
</div>
93-
)}
94-
</div>
95-
)}
96-
</div>
97-
</div>
98-
))}
99-
</div>
62+
<QueryTypesGrid
63+
items={queryTypesData.types.sort().map((name) => ({
64+
name,
65+
config: queryTypesData.configs[name],
66+
}))}
67+
/>
10068
)
10169
) : (
10270
<div className="rounded border border-red-200 bg-red-50 p-6 text-center">

0 commit comments

Comments
 (0)