feat: revue de code, doc CODE_REVIEW, forfaits 2026, traduction LLM, providers avec modèle

Made-with: Cursor
This commit is contained in:
Sepehr Ramezani
2026-03-07 11:42:58 +01:00
parent 3d37ce4582
commit 473b3e26c7
181 changed files with 30617 additions and 7170 deletions

View File

@@ -0,0 +1,217 @@
'use client';
import { useState, useEffect } from 'react';
import { Zap, Plus, AlertCircle } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Separator } from '@/components/ui/separator';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { useUser } from '@/app/dashboard/useUser';
import { useApiKeys } from './useApiKeys';
import { MAX_API_KEYS, type ApiKey } from './types';
import { ProUpgradePrompt } from './ProUpgradePrompt';
import { ApiKeyTable } from './ApiKeyTable';
import { GenerateKeyDialog } from './GenerateKeyDialog';
import { RevokeKeyDialog } from './RevokeKeyDialog';
import { WebhookSnippet } from './WebhookSnippet';
import { useToast } from '@/components/ui/toast';
export default function ApiKeysPage() {
const { data: user, isLoading: isLoadingUser } = useUser();
const {
keys,
total,
isLoading: isLoadingKeys,
isGenerating,
isRevoking,
generateKey,
revokeKey,
errorDetails,
parseGenerateError,
parseRevokeError,
} = useApiKeys();
const { toast } = useToast();
const [generateDialogOpen, setGenerateDialogOpen] = useState(false);
const [revokeDialogOpen, setRevokeDialogOpen] = useState(false);
const [keyToRevoke, setKeyToRevoke] = useState<{ id: string; name: string } | null>(null);
const [apiError, setApiError] = useState<string | null>(null);
const isPro = user?.tier === 'pro';
const maxKeysReached = total >= MAX_API_KEYS;
const isLoading = isLoadingUser || isLoadingKeys;
// Handle API errors with specific error codes
useEffect(() => {
if (errorDetails?.code === 'PRO_FEATURE_REQUIRED') {
// Redirect to upgrade prompt will happen via isPro check
setApiError(null);
} else if (errorDetails?.code === 'API_KEY_LIMIT_REACHED') {
setApiError('You have reached the maximum of 10 API keys. Revoke an existing key to generate a new one.');
} else if (errorDetails) {
setApiError(errorDetails.message);
} else {
setApiError(null);
}
}, [errorDetails]);
const handleRevokeClick = (key: ApiKey) => {
setKeyToRevoke({ id: key.id, name: key.name });
setRevokeDialogOpen(true);
};
const handleRevokeConfirm = async () => {
if (!keyToRevoke) return;
try {
await revokeKey(keyToRevoke.id);
setRevokeDialogOpen(false);
setKeyToRevoke(null);
toast({
title: 'Key revoked',
description: 'The API key has been revoked successfully.',
});
} catch (error) {
const revokeError = parseRevokeError();
if (revokeError?.code === 'API_KEY_NOT_FOUND') {
toast({
variant: 'destructive',
title: 'Key Not Found',
description: 'The API key no longer exists. It may have already been revoked.',
});
} else {
toast({
variant: 'destructive',
title: 'Error',
description: revokeError?.message || 'Failed to revoke the API key. Please try again.',
});
}
}
};
const handleGenerateKey = async (name?: string) => {
try {
const result = await generateKey(name);
return result;
} catch (error) {
const genError = parseGenerateError();
if (genError?.code === 'API_KEY_LIMIT_REACHED') {
toast({
variant: 'destructive',
title: 'Limit Reached',
description: 'You have reached the maximum of 10 API keys. Revoke an existing key to generate a new one.',
});
} else if (genError?.code === 'PRO_FEATURE_REQUIRED') {
toast({
variant: 'destructive',
title: 'Pro Feature Required',
description: 'API keys are a Pro feature. Please upgrade your account.',
});
} else {
toast({
variant: 'destructive',
title: 'Error',
description: genError?.message || 'Failed to generate API key. Please try again.',
});
}
throw error;
}
};
if (isLoading) {
return (
<div className="flex items-center justify-center min-h-[60vh]">
<div className="text-center space-y-4">
<div className="animate-spin rounded-full h-8 w-8 border-4 border-muted border-t-foreground mx-auto"></div>
<p className="text-sm text-muted-foreground">Loading...</p>
</div>
</div>
);
}
if (!isPro) {
return <ProUpgradePrompt />;
}
return (
<div className="space-y-6 p-6">
<div>
<h1 className="text-2xl font-semibold tracking-tight">API Keys</h1>
<p className="text-muted-foreground">
Manage your API keys for programmatic access to the translation API.
</p>
</div>
{apiError && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{apiError}</AlertDescription>
</Alert>
)}
<Card>
<CardHeader>
<div className="flex items-center gap-2">
<div className="flex size-8 items-center justify-center rounded-lg bg-accent/10">
<Zap className="size-4 text-accent" />
</div>
<div>
<CardTitle className="text-base">API & Automation</CardTitle>
<CardDescription>Generate and manage your API keys for automation workflows</CardDescription>
</div>
</div>
</CardHeader>
<CardContent className="space-y-6">
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium">
{total} of {MAX_API_KEYS} keys used
</p>
<p className="text-xs text-muted-foreground">
{maxKeysReached ? (
<span className="text-amber-600">Maximum keys reached. Revoke a key to generate a new one.</span>
) : (
`You can generate ${MAX_API_KEYS - total} more key${MAX_API_KEYS - total !== 1 ? 's' : ''}.`
)}
</p>
</div>
<Button
onClick={() => setGenerateDialogOpen(true)}
disabled={maxKeysReached || isGenerating}
className="gap-1.5"
>
<Plus className="size-3.5" />
Generate New Key
</Button>
</div>
<ApiKeyTable
keys={keys}
onRevoke={handleRevokeClick}
isRevoking={isRevoking}
/>
</CardContent>
</Card>
<Separator />
<WebhookSnippet />
<GenerateKeyDialog
open={generateDialogOpen}
onOpenChange={setGenerateDialogOpen}
onGenerate={handleGenerateKey}
isGenerating={isGenerating}
maxKeysReached={maxKeysReached}
/>
<RevokeKeyDialog
open={revokeDialogOpen}
onOpenChange={setRevokeDialogOpen}
onConfirm={handleRevokeConfirm}
isRevoking={isRevoking}
keyName={keyToRevoke?.name}
/>
</div>
);
}