Skip to content

Commit 67ee340

Browse files
committed
add API visit tracking, fetching, and syncing functionality
1 parent 0a264a7 commit 67ee340

File tree

15 files changed

+737
-507
lines changed

15 files changed

+737
-507
lines changed

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

Lines changed: 82 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,7 @@ import { Badge } from "@/components/ui/badge";
99
import JsonTreeContainer, { JsonTree } from "@/components/JsonTree";
1010
import ApiButtons from "@/components/ApiButtons";
1111
import VisitCounter from "@/components/VisitCounter";
12-
13-
interface ApiVersion {
14-
version: string;
15-
swaggerUrl: string;
16-
swaggerYamlUrl: string;
17-
}
12+
import type { ApiVersion } from "@/types/api";
1813

1914
function stripMarkdown(markdown: string): string {
2015
return markdown
@@ -37,85 +32,95 @@ export function getData(
3732
? `${providerSlug}:${serviceSlug}`
3833
: providerSlug;
3934

35+
if (apiList[targetKey]) {
36+
return processApiData(targetKey, apiList[targetKey]);
37+
}
38+
4039
for (const key in apiList) {
41-
if (apiList.hasOwnProperty(key) && key === targetKey) {
42-
try {
43-
const api = apiList[key];
44-
const versions = api.versions || {};
45-
const preferred = api.preferred || Object.keys(versions)[0] || "";
46-
const preferredVersion = versions[preferred] || {};
47-
const info = preferredVersion.info || {};
48-
const externalDocs = preferredVersion.externalDocs || {};
49-
const contact = info.contact || {};
50-
51-
const logo = {
52-
url: info["x-logo"]?.url || "/assets/images/no-logo.svg",
53-
backgroundColor: info["x-logo"]?.backgroundColor || null,
54-
};
55-
56-
const externalUrl =
57-
externalDocs.url ||
58-
contact.url ||
59-
(key.indexOf(".local") < 0 ? `https://${key.split(":")[0]}` : "");
60-
61-
let origUrl = "";
62-
if (
63-
info["x-origin"] &&
64-
Array.isArray(info["x-origin"]) &&
65-
info["x-origin"].length > 0
66-
) {
67-
origUrl =
68-
info["x-origin"][0]?.url || preferredVersion.swaggerUrl || "";
69-
} else {
70-
origUrl = preferredVersion.swaggerUrl || "";
71-
}
72-
73-
const categories = info["x-apisguru-categories"] || [];
74-
const tags = info["x-tags"] || [];
75-
76-
const versionsArray = Object.entries(versions).map(
77-
([version, details]: [string, any]) => ({
78-
version,
79-
swaggerUrl: details?.swaggerUrl || "",
80-
swaggerYamlUrl: details?.swaggerYamlUrl || "",
81-
})
82-
);
83-
84-
const description = info.description || "No description available";
85-
const cardDescription = marked(description);
86-
const cardDescriptionPlain = stripMarkdown(description);
87-
88-
return {
89-
name: key,
90-
preferred: api.preferred || "",
91-
info,
92-
api: {
93-
swaggerUrl: preferredVersion.swaggerUrl || "",
94-
swaggerYamlUrl: preferredVersion.swaggerYamlUrl || "",
95-
},
96-
logo,
97-
externalUrl,
98-
origUrl,
99-
versions: versionsArray,
100-
cardDescription,
101-
cardDescriptionPlain,
102-
categories,
103-
tags,
104-
integrations: api.integrations || [],
105-
updated: preferredVersion.updated || "", // Include the updated field
106-
};
107-
} catch (error) {
108-
console.error(`Error processing API ${key}:`, error);
109-
return null;
110-
}
40+
if (
41+
apiList.hasOwnProperty(key) &&
42+
key.toLowerCase() === targetKey.toLowerCase()
43+
) {
44+
return processApiData(key, apiList[key]);
11145
}
11246
}
47+
11348
console.warn(
11449
`No API found for provider: ${providerSlug}, service: ${serviceSlug}`
11550
);
11651
return null;
11752
}
11853

54+
function processApiData(key: string, api: any) {
55+
try {
56+
const versions = api.versions || {};
57+
const preferred = api.preferred || Object.keys(versions)[0] || "";
58+
const preferredVersion = versions[preferred] || {};
59+
const info = preferredVersion.info || {};
60+
const externalDocs = preferredVersion.externalDocs || {};
61+
const contact = info.contact || {};
62+
63+
const logo = {
64+
url: info["x-logo"]?.url || "/assets/images/no-logo.svg",
65+
backgroundColor: info["x-logo"]?.backgroundColor || null,
66+
};
67+
68+
const externalUrl =
69+
externalDocs.url ||
70+
contact.url ||
71+
(key.indexOf(".local") < 0 ? `https://${key.split(":")[0]}` : "");
72+
73+
let origUrl = "";
74+
if (
75+
info["x-origin"] &&
76+
Array.isArray(info["x-origin"]) &&
77+
info["x-origin"].length > 0
78+
) {
79+
origUrl = info["x-origin"][0]?.url || preferredVersion.swaggerUrl || "";
80+
} else {
81+
origUrl = preferredVersion.swaggerUrl || "";
82+
}
83+
84+
const categories = info["x-apisguru-categories"] || [];
85+
const tags = info["x-tags"] || [];
86+
87+
const versionsArray = Object.entries(versions).map(
88+
([version, details]: [string, any]) => ({
89+
version,
90+
swaggerUrl: details?.swaggerUrl || "",
91+
swaggerYamlUrl: details?.swaggerYamlUrl || "",
92+
})
93+
);
94+
95+
const description = info.description || "No description available";
96+
const cardDescription = marked(description);
97+
const cardDescriptionPlain = stripMarkdown(description);
98+
99+
return {
100+
name: key,
101+
preferred: api.preferred || "",
102+
info,
103+
api: {
104+
swaggerUrl: preferredVersion.swaggerUrl || "",
105+
swaggerYamlUrl: preferredVersion.swaggerYamlUrl || "",
106+
},
107+
logo,
108+
externalUrl,
109+
origUrl,
110+
versions: versionsArray,
111+
cardDescription,
112+
cardDescriptionPlain,
113+
categories,
114+
tags,
115+
integrations: api.integrations || [],
116+
updated: preferredVersion.updated || "",
117+
};
118+
} catch (error) {
119+
console.error(`Error processing API ${key}:`, error);
120+
return null;
121+
}
122+
}
123+
119124
export async function generateStaticParams() {
120125
const apiList = list as Record<string, any>;
121126
const params: { providerSlug: string; serviceSlug: string }[] = [];
@@ -213,10 +218,7 @@ export default async function ApiPage({
213218

214219
return (
215220
<div className="container mx-auto p-6 max-w-4xl">
216-
<VisitCounter
217-
providerSlug={providerSlug}
218-
serviceSlug={serviceSlug}
219-
/>
221+
<VisitCounter providerSlug={providerSlug} serviceSlug={serviceSlug} />
220222

221223
<div className="flex flex-col md:flex-row gap-6 mb-8">
222224
<div className="flex-shrink-0">

components/ApiCard.tsx

Lines changed: 9 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { ApiCardModel } from "../models/ApiCardModel";
33
import { Card, CardContent, CardHeader } from "./ui/card";
44
import Link from "next/link";
55
import Image from "next/image";
6+
import type { ApiCard } from "@/types/api";
67

7-
export default function ApiCard({ model }: { model: ApiCardModel }) {
8+
export default function ApiCard({ model }: { model: ApiCard }) {
89
const [provider, service] = model.name.split(":");
910

1011
const providerSlug = provider.toLowerCase();
@@ -19,45 +20,26 @@ export default function ApiCard({ model }: { model: ApiCardModel }) {
1920
className="block hover:scale-105 transition-transform duration-200"
2021
>
2122
<Card className="flex flex-col text-center border border-[#388c9a] rounded-md bg-[#eee] overflow-hidden h-full cursor-pointer hover:shadow-lg transition-shadow duration-200">
22-
{model.classes ? (
23-
<span
24-
className={`block w-[125px] py-1 px-5 relative text-center text-black top-[15px] -left-[28px] rotate-[-45deg] opacity-75 ${
25-
model.classes.includes("flash-green")
26-
? "bg-[#2c7]"
27-
: model.classes.includes("flash-yellow")
28-
? "bg-[#fed16e]"
29-
: model.classes.includes("flash-red")
30-
? "bg-red-500"
31-
: ""
32-
}`}
33-
title={model.flashTitle}
34-
>
35-
<strong>{model.flashText}</strong>
36-
</span>
37-
) : (
38-
<span className=""></span>
39-
)}
40-
23+
<span className=""></span>
4124
<CardHeader className="text-[#388c9a] pb-2">
4225
<h2
4326
className="font-normal mb-0.5 whitespace-nowrap text-ellipsis overflow-hidden"
44-
title={model.info.title}
27+
title={model.title}
4528
>
46-
{model.info.title}
29+
{model.title}
4730
</h2>
48-
<p className="text-sm text-gray-600">v{model.preferred}</p>
31+
<p className="text-sm text-gray-600">v{model.version}</p>
4932
</CardHeader>
50-
5133
<CardContent className="p-[15px] bg-white flex-grow flex flex-col justify-between">
5234
<div>
5335
<Image
54-
src={model.logo.url || "/assets/images/no-logo.svg"}
55-
alt={`${model.info.title} API logo`}
36+
src={model.logoUrl || "/assets/images/no-logo.svg"}
37+
alt={`${model.title} API logo`}
5638
width={80}
5739
height={80}
5840
className="max-w-full max-h-[80px] p-[5px] mx-auto mb-4"
5941
style={{
60-
backgroundColor: model.logo.backgroundColor || "transparent",
42+
backgroundColor: "transparent",
6143
}}
6244
/>
6345
<p className="leading-[1.2] overflow-hidden text-ellipsis h-[calc(1em*1.2*3)] text-sm text-gray-700">

components/ApiGrid.tsx

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
import React from "react";
2-
import { ApiCardModel } from "@/models/ApiCardModel";
3-
import Card from "@/components/Card";
2+
3+
import Card from "./ApiCard";
4+
45
import { CardSkeleton } from "@/components/ui/CardSkeleton";
6+
import { ApiCard } from "@/types/api";
57

68
interface ApiGridProps {
7-
cards: ApiCardModel[];
9+
cards: ApiCard[];
810
searchTerm: string;
911
loading: boolean;
1012
loadingMore: boolean;
@@ -26,7 +28,6 @@ export function ApiGrid({
2628
}: ApiGridProps) {
2729
return (
2830
<section id="apis-list" className="cards">
29-
{/* Show skeletons when loading initial data */}
3031
{loading ? (
3132
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-4">
3233
{Array.from({ length: Math.min(pageSize, gridColumns * 2) }).map(
@@ -37,7 +38,6 @@ export function ApiGrid({
3738
</div>
3839
) : (
3940
<>
40-
{/* Show skeletons when loading more data */}
4141
{loadingMore && (
4242
<div className="grid grid-cols-1 md:grid-cols-3 lg:grid-cols-5 gap-4 mt-4">
4343
{Array.from({ length: Math.min(pageSize, gridColumns * 2) }).map(
@@ -61,10 +61,8 @@ export function ApiGrid({
6161
</>
6262
)}
6363

64-
{/* Intersection observer target */}
6564
<div ref={observerRef} className="h-10 mt-4" />
6665

67-
{/* End of results indicator */}
6866
{!hasMore && cards.length > 0 && (
6967
<div className="text-center py-6 text-gray-500">
7068
That's all the APIs! 🎉

components/Card.tsx

Lines changed: 0 additions & 7 deletions
This file was deleted.

db/schema.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";
2+
3+
export const apis = sqliteTable("Apis", {
4+
name: text("name").notNull().primaryKey(),
5+
description: text("description"),
6+
title: text("title").notNull(),
7+
categories: text("categories"),
8+
tags: text("tags"),
9+
contact: text("contact"),
10+
license: text("license"),
11+
logoUrl: text("logoUrl"),
12+
swaggerUrl: text("swaggerUrl"),
13+
swaggerYamlUrl: text("swaggerYamlUrl"),
14+
externalUrl: text("externalUrl"),
15+
version: text("version"),
16+
added: text("added").notNull(),
17+
updated: text("updated").notNull(),
18+
});
19+
20+
export const apiVisits = sqliteTable("ApiVisits", {
21+
api_name: text("api_name").notNull().primaryKey(),
22+
visits: integer("visits").notNull().default(0),
23+
});

0 commit comments

Comments
 (0)