Skip to content

Commit 57b658b

Browse files
committed
add new JsonTreeActions component and dropdown copy/dowload menus
1 parent 99a1855 commit 57b658b

File tree

5 files changed

+495
-140
lines changed

5 files changed

+495
-140
lines changed

app/apis/[providerSlug]/[serviceSlug]/page.tsx

Lines changed: 77 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import JsonTreeContainer, { JsonTree } from "@/components/JsonTree";
1010
import ApiButtons from "@/components/ApiButtons";
1111
import VisitCounter from "@/components/VisitCounter";
1212
import type { ApiVersion } from "@/types/api";
13+
import { ExternalLink } from "lucide-react";
1314

1415
function stripMarkdown(markdown: string): string {
1516
return markdown
@@ -24,7 +25,7 @@ function stripMarkdown(markdown: string): string {
2425
}
2526
export function getData(
2627
providerSlug: string,
27-
serviceSlug?: string | null,
28+
serviceSlug?: string | null
2829
): any | null {
2930
const apiList = list as Record<string, any>;
3031

@@ -46,7 +47,7 @@ export function getData(
4647
}
4748

4849
console.warn(
49-
`No API found for provider: ${providerSlug}, service: ${serviceSlug}`,
50+
`No API found for provider: ${providerSlug}, service: ${serviceSlug}`
5051
);
5152
return null;
5253
}
@@ -89,7 +90,7 @@ function processApiData(key: string, api: any) {
8990
version,
9091
swaggerUrl: details?.swaggerUrl || "",
9192
swaggerYamlUrl: details?.swaggerYamlUrl || "",
92-
}),
93+
})
9394
);
9495

9596
const description = info.description || "No description available";
@@ -138,15 +139,14 @@ export async function generateStaticParams() {
138139
}
139140
}
140141

141-
142142
return params;
143143
}
144144

145145
export async function generateMetadata(
146146
{
147147
params,
148148
}: { params: Promise<{ providerSlug: string; serviceSlug: string }> },
149-
parent: ResolvingMetadata,
149+
parent: ResolvingMetadata
150150
): Promise<Metadata> {
151151
const { providerSlug, serviceSlug } = await params;
152152
const api = getData(providerSlug, serviceSlug);
@@ -217,72 +217,73 @@ export default async function ApiPage({
217217
}
218218

219219
return (
220-
<div className="container mx-auto p-6 max-w-4xl">
220+
<div className="container mx-auto p-6 max-w-6xl">
221221
<VisitCounter providerSlug={providerSlug} serviceSlug={serviceSlug} />
222222

223223
<div className="flex flex-col md:flex-row gap-6 mb-8">
224224
<div className="flex-shrink-0">
225-
<Image
226-
src={api.logo.url}
227-
alt={`${api.info.title} API logo`}
228-
width={200}
229-
height={200}
230-
className="max-w-full max-h-[200px] p-[10px]"
231-
style={{
232-
backgroundColor: api.logo.backgroundColor || "transparent",
233-
}}
234-
/>
225+
<div className="bg-white rounded-lg px-6 ">
226+
<Image
227+
src={api.logo.url}
228+
alt={`${api.info.title} API logo`}
229+
width={200}
230+
height={200}
231+
className="max-w-full max-h-[200px] mx-auto"
232+
style={{
233+
backgroundColor: api.logo.backgroundColor || "transparent",
234+
}}
235+
/>
236+
</div>
235237
</div>
236238

237239
<div className="flex-grow">
238-
<div className="text-3xl font-bold text-[#388c9a] mb-2 gap-6 flex items-center">
239-
{api.externalUrl ? (
240-
<Link
241-
href={api.externalUrl}
242-
target="_blank"
243-
className="hover:underline text-decoration-line:none text-[#388c9a]"
244-
>
245-
{api.info.title}
246-
</Link>
247-
) : (
248-
api.info.title
249-
)}
250-
{/* <Badge variant="outline" className="text-sm">
251-
<span className="text-sm text-gray-500">OpenAPI / Swagger</span>
252-
</Badge> */}
253-
</div>
240+
<div className="mb-6">
241+
<h1 className="text-4xl font-bold text-gray-900 mb-3">
242+
{api.externalUrl ? (
243+
<Link
244+
href={api.externalUrl}
245+
target="_blank"
246+
className="hover:text-[#388c9a] transition-colors duration-200"
247+
>
248+
{api.info.title}
249+
</Link>
250+
) : (
251+
api.info.title
252+
)}
253+
</h1>
254+
255+
<p className="text-lg text-gray-600 mb-4">
256+
Last updated:{" "}
257+
{new Date(api.updated).toLocaleString("en-US", {
258+
year: "numeric",
259+
month: "long",
260+
day: "numeric",
261+
})}
262+
</p>
254263

255-
<h3 className="text-lg mb-4">
256-
Last updated at:{" "}
257-
{new Date(api.updated).toLocaleString("en-US", {
258-
year: "numeric",
259-
month: "long",
260-
day: "numeric",
261-
})}
262-
</h3>
263-
264-
<div className="relative flex flex-wrap gap-3 mb-6">
265-
<div className="flex flex-wrap gap-3">
264+
<div className="mb-6">
266265
<ApiButtons
267266
swaggerUrl={api.api.swaggerUrl}
268-
swaggerYamlUrl={api.api.swaggerYamlUrl}
269-
origUrl={api.origUrl}
270267
title={api.info.title}
271268
/>
272269
</div>
273270
</div>
271+
274272
{api.integrations && api.integrations.length > 0 && (
275273
<div className="mb-6">
276-
<h4 className="text-lg font-semibold mb-3">Tools</h4>
277-
<div className="flex flex-wrap gap-2">
274+
<h3 className="text-xl font-semibold mb-3 text-gray-900">
275+
Available Tools
276+
</h3>
277+
<div className="flex flex-wrap gap-3">
278278
{api.integrations.map((integration: any, index: any) => (
279279
<Link
280280
key={index}
281281
href={integration.template}
282282
target="_blank"
283-
className="py-1 px-3 bg-gray-600 rounded text-white text-sm hover:bg-gray-700"
283+
className="inline-flex items-center px-4 py-2 bg-gray-100 hover:bg-gray-200 text-gray-800 rounded-lg transition-colors duration-200 text-sm font-medium"
284284
>
285285
{integration.text}
286+
<ExternalLink className="w-3 h-3 ml-1" />
286287
</Link>
287288
))}
288289
</div>
@@ -294,29 +295,48 @@ export default async function ApiPage({
294295
<DescriptionSection description={api.cardDescription} />
295296

296297
<div className="mb-8">
297-
<h2 className="text-2xl font-bold mb-4">OpenAPI/Swagger JSON</h2>
298-
299-
<JsonTreeContainer swaggerUrl={api.api.swaggerUrl} />
298+
<h2 className="text-3xl font-bold mb-6 text-gray-900">
299+
OpenAPI Specification
300+
</h2>
301+
<JsonTreeContainer
302+
swaggerUrl={api.api.swaggerUrl}
303+
swaggerYamlUrl={api.api.swaggerYamlUrl}
304+
title={api.info.title}
305+
/>
300306
</div>
301307

302308
{api.versions && api.versions.length > 0 && (
303309
<div>
304-
<h2 className="text-2xl font-bold mb-4">All Versions</h2>
310+
<h2 className="text-3xl font-bold mb-6 text-gray-900">
311+
All Versions
312+
</h2>
305313
<div className="space-y-4">
306314
{api.versions
307315
.reverse()
308316
.map((version: ApiVersion, index: number) => (
309317
<div
310318
key={index}
311-
className="flex items-center justify-between p-4 bg-gray-50 rounded"
319+
className="bg-white border rounded-lg p-6 shadow-sm"
312320
>
313-
<span className="font-semibold">{version.version}</span>
314-
<div className="flex gap-2">
321+
<div className="flex items-center justify-between mb-4">
322+
<h3 className="text-xl font-semibold text-gray-900">
323+
Version {version.version}
324+
</h3>
315325
<ApiButtons
326+
swaggerUrl={version.swaggerUrl}
327+
title={api.info.title}
328+
version={version.version}
329+
/>
330+
</div>
331+
332+
<div className="mb-4">
333+
<h4 className="text-lg font-medium mb-3 text-gray-800">
334+
OpenAPI Specification
335+
</h4>
336+
<JsonTreeContainer
316337
swaggerUrl={version.swaggerUrl}
317338
swaggerYamlUrl={version.swaggerYamlUrl}
318-
origUrl={`https://redocly.github.io/redoc/?url=${version.swaggerUrl}`}
319-
title={`${api.info.title}-v${version.version}`}
339+
title={api.info.title}
320340
version={version.version}
321341
/>
322342
</div>

components/ApiButtons.tsx

Lines changed: 15 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -1,100 +1,33 @@
11
"use client";
22

33
import React from "react";
4-
import { Download, FileText } from "lucide-react";
4+
import { FileText, ExternalLink } from "lucide-react";
55

66
interface ApiButtonsProps {
77
swaggerUrl: string;
8-
swaggerYamlUrl: string;
9-
origUrl: string;
108
title: string;
119
version?: string;
1210
}
1311

14-
async function downloadFile(url: string, filename: string) {
15-
try {
16-
const response = await fetch(url);
17-
if (!response.ok) throw new Error("Failed to fetch file");
18-
const blob = await response.blob();
19-
const downloadUrl = window.URL.createObjectURL(blob);
20-
const a = document.createElement("a");
21-
a.href = downloadUrl;
22-
a.download = filename;
23-
document.body.appendChild(a);
24-
a.click();
25-
document.body.removeChild(a);
26-
window.URL.revokeObjectURL(downloadUrl);
27-
} catch (error) {
28-
console.error("Download failed:", error);
29-
alert("Failed to download file. Please try again.");
30-
}
31-
}
32-
3312
export default function ApiButtons({
3413
swaggerUrl,
35-
swaggerYamlUrl,
36-
origUrl,
3714
title,
3815
version,
3916
}: ApiButtonsProps) {
40-
return (
41-
<div className="flex flex-wrap gap-3">
42-
<button
43-
onClick={() =>
44-
downloadFile(
45-
swaggerUrl,
46-
`${title}${version ? `-v${version}` : ""}-swagger.json`,
47-
)
48-
}
49-
className="py-2 px-4 bg-[#388c9a] rounded text-white hover:bg-[#2a6b77] flex items-center gap-2 transition-colors duration-200"
50-
title={`Download JSON${version ? ` for version ${version}` : ""}`}
51-
>
52-
<Download className="w-4 h-4" />
53-
JSON
54-
</button>
17+
const handleViewDocs = () => {
18+
const url = `https://redocly.github.io/redoc/?url=${swaggerUrl}`;
19+
window.open(url, "_blank");
20+
};
5521

56-
<button
57-
onClick={() =>
58-
downloadFile(
59-
swaggerYamlUrl,
60-
`${title}${version ? `-v${version}` : ""}-swagger.yaml`,
61-
)
62-
}
63-
className="py-2 px-4 bg-[#388c9a] rounded text-white hover:bg-[#2a6b77] flex items-center gap-2 transition-colors duration-200"
64-
title={`Download YAML${version ? ` for version ${version}` : ""}`}
65-
>
66-
<Download className="w-4 h-4" />
67-
YAML
68-
</button>
69-
70-
<button
71-
onClick={() =>
72-
downloadFile(
73-
origUrl,
74-
`${title}${version ? `-v${version}` : ""}-original.json`,
75-
)
76-
}
77-
className="py-2 px-4 bg-[#388c9a] rounded text-white hover:bg-[#2a6b77] flex items-center gap-2 transition-colors duration-200"
78-
title={`Download original${version ? ` for version ${version}` : ""}`}
79-
>
80-
<Download className="w-4 h-4" />
81-
Original
82-
</button>
83-
84-
<button
85-
onClick={() => {
86-
const url = `https://redocly.github.io/redoc/?url=${swaggerUrl}`;
87-
const a = document.createElement("a");
88-
a.href = url;
89-
a.download = `${title}${version ? `-v${version}` : ""}-docs.html`;
90-
a.click();
91-
}}
92-
className="py-2 px-4 bg-[#388c9a] rounded text-white hover:bg-[#2a6b77] flex items-center gap-2 transition-colors duration-200"
93-
title={`Download documentation${version ? ` for version ${version}` : ""}`}
94-
>
95-
<FileText className="w-4 h-4" />
96-
Documentation
97-
</button>
98-
</div>
22+
return (
23+
<button
24+
onClick={handleViewDocs}
25+
className="inline-flex items-center gap-2 px-6 py-3 bg-gradient-to-r from-[#388c9a] to-[#2a6b77] text-white font-medium rounded-lg hover:from-[#2a6b77] hover:to-[#1e4d56] transition-all duration-200 shadow-lg hover:shadow-xl transform hover:-translate-y-0.5"
26+
title={`View documentation${version ? ` for version ${version}` : ""}`}
27+
>
28+
<FileText className="w-5 h-5" />
29+
View Documentation
30+
<ExternalLink className="w-4 h-4" />
31+
</button>
9932
);
10033
}

components/JsonTree.tsx

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import { JSONTree } from "react-json-tree";
44
import { ReactNode, useState, useEffect } from "react";
5+
import React from "react";
6+
import JsonTreeActions from "./JsonTreeActions";
57

68
// Define the theme for JSONTree
79
const theme = {
@@ -56,8 +58,14 @@ export function JsonTree({ jsonData }: { jsonData: any }) {
5658

5759
export default function JsonTreeContainer({
5860
swaggerUrl,
61+
swaggerYamlUrl,
62+
title,
63+
version,
5964
}: {
6065
swaggerUrl: string;
66+
swaggerYamlUrl?: string;
67+
title: string;
68+
version?: string;
6169
}) {
6270
const [jsonData, setJsonData] = useState<any>(null);
6371
const [error, setError] = useState<string | null>(null);
@@ -93,5 +101,16 @@ export default function JsonTreeContainer({
93101
return <div className="text-red-500">{error}</div>;
94102
}
95103

96-
return <JsonTree jsonData={jsonData} />;
104+
return (
105+
<div className="relative bg-[#272822] p-4 rounded-lg max-h-[600px] overflow-auto json-tree-container">
106+
<JsonTreeActions
107+
swaggerUrl={swaggerUrl}
108+
swaggerYamlUrl={swaggerYamlUrl || swaggerUrl.replace(".json", ".yaml")}
109+
title={title}
110+
version={version}
111+
jsonData={jsonData}
112+
/>
113+
<JsonTree jsonData={jsonData} />
114+
</div>
115+
);
97116
}

0 commit comments

Comments
 (0)