Initial commit

This commit is contained in:
decolua
2026-01-05 09:58:59 +07:00
commit 3857598de4
159 changed files with 14537 additions and 0 deletions
@@ -0,0 +1,235 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardSkeleton, Badge } from "@/shared/components";
import { OAUTH_PROVIDERS, APIKEY_PROVIDERS } from "@/shared/constants/config";
import Image from "next/image";
import Link from "next/link";
import { getErrorCode, getRelativeTime } from "@/shared/utils";
export default function ProvidersPage() {
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetchData();
}, []);
const fetchData = async () => {
try {
const res = await fetch("/api/providers");
const data = await res.json();
if (res.ok) setConnections(data.connections || []);
} catch (error) {
console.log("Error fetching data:", error);
} finally {
setLoading(false);
}
};
const getProviderStats = (providerId, authType) => {
const providerConnections = connections.filter(
c => c.provider === providerId && c.authType === authType
);
// Helper: check if connection is effectively active (cooldown expired)
const getEffectiveStatus = (conn) => {
const isCooldown = conn.rateLimitedUntil && new Date(conn.rateLimitedUntil).getTime() > Date.now();
return (conn.testStatus === "unavailable" && !isCooldown) ? "active" : conn.testStatus;
};
const connected = providerConnections.filter(c => {
const status = getEffectiveStatus(c);
return status === "active" || status === "success";
}).length;
const errorConns = providerConnections.filter(c => {
const status = getEffectiveStatus(c);
return status === "error" || status === "expired" || status === "unavailable";
});
const error = errorConns.length;
const total = providerConnections.length;
// Get latest error info
const latestError = errorConns.sort((a, b) =>
new Date(b.lastErrorAt || 0) - new Date(a.lastErrorAt || 0)
)[0];
const errorCode = latestError ? getErrorCode(latestError.lastError) : null;
const errorTime = latestError?.lastErrorAt ? getRelativeTime(latestError.lastErrorAt) : null;
return { connected, error, total, errorCode, errorTime };
};
if (loading) {
return (
<div className="flex flex-col gap-8">
<CardSkeleton />
<CardSkeleton />
</div>
);
}
return (
<div className="flex flex-col gap-8">
{/* OAuth Providers */}
<div className="flex flex-col gap-4">
<h2 className="text-xl font-semibold">OAuth Providers</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{Object.entries(OAUTH_PROVIDERS).map(([key, info]) => (
<ProviderCard
key={key}
providerId={key}
provider={info}
stats={getProviderStats(key, "oauth")}
/>
))}
</div>
</div>
{/* API Key Providers */}
<div className="flex flex-col gap-4">
<h2 className="text-xl font-semibold">API Key Providers</h2>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{Object.entries(APIKEY_PROVIDERS).map(([key, info]) => (
<ApiKeyProviderCard
key={key}
providerId={key}
provider={info}
stats={getProviderStats(key, "apikey")}
/>
))}
</div>
</div>
</div>
);
}
function ProviderCard({ providerId, provider, stats }) {
const { connected, error, errorCode, errorTime } = stats;
const [imgError, setImgError] = useState(false);
const getStatusDisplay = () => {
const parts = [];
if (connected > 0) {
parts.push(
<Badge key="connected" variant="success" size="sm" dot>
{connected} Connected
</Badge>
);
}
if (error > 0) {
const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`;
parts.push(
<Badge key="error" variant="error" size="sm" dot>
{errText}
</Badge>
);
}
if (parts.length === 0) {
return <span className="text-text-muted">No connections</span>;
}
return parts;
};
return (
<Link href={`/dashboard/providers/${providerId}`}>
<Card className="h-full hover:border-primary/50 transition-colors cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="size-10 rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${provider.color}15` }}
>
{!imgError ? (
<img
src={`/providers/${provider.id}.png`}
alt={provider.name}
className="size-10 object-contain rounded-lg"
onError={() => setImgError(true)}
/>
) : (
<span
className="text-sm font-bold"
style={{ color: provider.color }}
>
{provider.textIcon || provider.id.slice(0, 2).toUpperCase()}
</span>
)}
</div>
<div>
<h3 className="font-semibold">{provider.name}</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
{getStatusDisplay()}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
</div>
</div>
</div>
<span className="material-symbols-outlined text-text-muted">
chevron_right
</span>
</div>
</Card>
</Link>
);
}
// API Key providers - only use textIcon, no image
function ApiKeyProviderCard({ providerId, provider, stats }) {
const { connected, error, errorCode, errorTime } = stats;
const getStatusDisplay = () => {
const parts = [];
if (connected > 0) {
parts.push(
<Badge key="connected" variant="success" size="sm" dot>
{connected} Connected
</Badge>
);
}
if (error > 0) {
const errText = errorCode ? `${error} Error (${errorCode})` : `${error} Error`;
parts.push(
<Badge key="error" variant="error" size="sm" dot>
{errText}
</Badge>
);
}
if (parts.length === 0) {
return <span className="text-text-muted">No connections</span>;
}
return parts;
};
return (
<Link href={`/dashboard/providers/${providerId}`}>
<Card className="h-full hover:border-primary/50 transition-colors cursor-pointer">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div
className="size-10 rounded-lg flex items-center justify-center"
style={{ backgroundColor: `${provider.color}15` }}
>
<span
className="text-sm font-bold"
style={{ color: provider.color }}
>
{provider.textIcon || provider.id.slice(0, 2).toUpperCase()}
</span>
</div>
<div>
<h3 className="font-semibold">{provider.name}</h3>
<div className="flex items-center gap-2 text-xs flex-wrap">
{getStatusDisplay()}
{errorTime && <span className="text-text-muted"> {errorTime}</span>}
</div>
</div>
</div>
<span className="material-symbols-outlined text-text-muted">
chevron_right
</span>
</div>
</Card>
</Link>
);
}