Currency Api Dashboard

TypeScript

Modern currency exchange rate dashboard with real-time market data, conversion tools, analytics, historical trends, and responsive admin interface.

Stars
18
Forks
1
Downloads
N/A
Open Issues
0
Files main

Repository Files

Loading file structure...
resources/js/pages/dashboard/tokens.tsx
import { Head, useForm, router } from '@inertiajs/react';
import {
    Plus,
    Copy,
    Check,
    Eye,
    EyeOff,
    RefreshCw,
    Power,
    Trash2,
    Database,
    Calendar,
} from 'lucide-react';
import React, { useState } from 'react';
import { toast } from 'sonner';
import { Button } from '@/components/ui/button';
import { Combobox } from '@/components/ui/combobox';
import {
    Dialog,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
} from '@/components/ui/dialog';

import { store, regenerate, toggle, destroy } from '@/routes/tokens';

interface Currency {
    id: number;
    code: string;
    name: string;
    country: string | null;
    rate: number;
    active: boolean;
}

interface Token {
    id: number;
    name: string;
    token: string;
    default_currency: string;
    active: boolean;
    created_at: string;
    updated_at: string;
}

interface TokensPageProps {
    tokens: Token[];
    currencies: Currency[];
}

// Stable locale-independent date formatter to prevent hydration mismatch
const formatDate = (dateStr: string) => {
    const d = new Date(dateStr);

    if (isNaN(d.getTime())) {
        return dateStr;
    }

    const year = d.getFullYear();
    const month = String(d.getMonth() + 1).padStart(2, '0');
    const day = String(d.getDate()).padStart(2, '0');

    return `${day}/${month}/${year}`;
};

export default function TokensPage({
    tokens = [],
    currencies = [],
}: TokensPageProps) {
    const [isCreateOpen, setIsCreateOpen] = useState(false);
    const [visibleTokens, setVisibleTokens] = useState<Record<number, boolean>>(
        {},
    );
    const [copiedTokenId, setCopiedTokenId] = useState<number | null>(null);

    // Form for creating a new token - default_currency starts as empty to fallback to INR
    const { data, setData, post, processing, errors, reset } = useForm({
        name: '',
        default_currency: '',
    });

    // Toggle token key visibility
    const toggleTokenVisibility = (id: number) => {
        setVisibleTokens((prev) => ({ ...prev, [id]: !prev[id] }));
    };

    // Copy token to clipboard
    const copyToClipboard = (text: string, id: number) => {
        navigator.clipboard.writeText(text);
        setCopiedTokenId(id);
        toast.success('Token copied to clipboard!');
        setTimeout(() => setCopiedTokenId(null), 2000);
    };

    // Submit new token
    const handleCreateToken = (e: React.FormEvent) => {
        e.preventDefault();
        post(store.url(), {
            onSuccess: () => {
                setIsCreateOpen(false);
                reset('name', 'default_currency');
                toast.success('New API key registered successfully!');
            },
            onError: () => {
                toast.error('Token creation failed. Please check your inputs.');
            },
        });
    };

    // Regenerate token key
    const handleRegenerateToken = (id: number, name: string) => {
        if (
            confirm(
                `Are you sure you want to regenerate the API key for "${name}"? The existing key will stop working immediately.`,
            )
        ) {
            router.post(
                regenerate.url(id),
                {},
                {
                    onSuccess: () =>
                        toast.success('API key regenerated successfully!'),
                    onError: () => toast.error('Failed to regenerate key.'),
                },
            );
        }
    };

    // Toggle active status
    const handleToggleStatus = (id: number) => {
        router.patch(
            toggle.url(id),
            {},
            {
                onSuccess: () => toast.success('Key active state toggled.'),
                onError: () => toast.error('Failed to toggle active status.'),
            },
        );
    };

    // Revoke/Delete token
    const handleRevokeToken = (id: number, name: string) => {
        if (
            confirm(
                `Are you sure you want to revoke and delete "${name}"? This key will be deactivated permanently.`,
            )
        ) {
            router.delete(destroy.url(id), {
                onSuccess: () =>
                    toast.success('API key has been permanently deleted.'),
                onError: () => toast.error('Failed to delete key.'),
            });
        }
    };

    // Prepare combobox options list
    const currencyOptions = React.useMemo(() => {
        const list = [
            {
                value: '',
                label: 'INR (System Default Fallback)',
                searchTerms: 'INR Rupee India default fallback',
            },
        ];
        currencies.forEach((c) => {
            list.push({
                value: c.code,
                label: `${c.code} - ${c.name}`,
                searchTerms: `${c.code} ${c.name} ${c.country || ''}`,
            });
        });

        return list;
    }, [currencies]);

    return (
        <>
            <Head title="API Access Tokens" />
            <div className="min-h-screen w-full space-y-6 bg-background p-4 text-foreground md:p-6">
                {/* Header */}
                <div className="flex flex-col gap-4 border-b border-border pb-6 sm:flex-row sm:items-center sm:justify-between">
                    <div>
                        <div className="flex items-center gap-2">
                            <span className="rounded-lg bg-primary/10 p-1.5 text-primary">
                                <Database className="h-5 w-5" />
                            </span>
                            <h1 className="text-3xl font-extrabold tracking-tight text-foreground">
                                API Access Tokens
                            </h1>
                        </div>
                        <p className="mt-1 text-sm text-muted-foreground">
                            Generate and manage secure authorization keys to
                            fetch currency exchange rates.
                        </p>
                    </div>
                    <Button
                        onClick={() => setIsCreateOpen(true)}
                        className="bg-primary font-medium text-primary-foreground shadow-md hover:bg-primary/90"
                    >
                        <Plus className="mr-2 h-4 w-4" /> Generate API Token
                    </Button>
                </div>

                {/* Tokens List Content (Full Width) */}
                <div className="space-y-4">
                    {tokens.length === 0 ? (
                        <div className="rounded-lg border border-dashed border-border bg-muted/10 py-16 text-center">
                            <Database className="mx-auto mb-2 h-12 w-12 text-muted-foreground/60" />
                            <p className="text-sm font-semibold text-muted-foreground">
                                No active API keys found.
                            </p>
                            <p className="mt-1 text-xs text-muted-foreground">
                                Create a new key to authorize request hits on
                                the endpoints.
                            </p>
                        </div>
                    ) : (
                        <div className="space-y-4">
                            {tokens.map((token) => (
                                <div
                                    key={token.id}
                                    className="relative flex flex-col gap-3 rounded-lg border border-border bg-muted/10 p-4 transition-colors hover:border-muted"
                                >
                                    <div className="flex items-center justify-between">
                                        <span className="text-sm font-bold text-foreground">
                                            {token.name}
                                        </span>
                                        <div className="flex items-center gap-2">
                                            <span
                                                className={`rounded-full px-2.5 py-0.5 text-[10px] font-semibold ${
                                                    token.active
                                                        ? 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400'
                                                        : 'bg-muted text-muted-foreground'
                                                }`}
                                            >
                                                {token.active
                                                    ? 'Active'
                                                    : 'Disabled'}
                                            </span>
                                            <span className="rounded-full bg-primary/10 px-2.5 py-0.5 font-mono text-[10px] font-bold text-primary">
                                                Base Fallback:{' '}
                                                {token.default_currency}
                                            </span>
                                        </div>
                                    </div>

                                    {/* Masked token input */}
                                    <div className="flex items-center gap-1 rounded-md border border-border bg-card p-2 font-mono text-xs">
                                        <span className="flex-1 truncate px-1 select-all">
                                            {visibleTokens[token.id]
                                                ? token.token
                                                : `${token.token.substring(0, 8)}****************************************`}
                                        </span>
                                        <Button
                                            size="icon"
                                            variant="ghost"
                                            className="h-7 w-7 text-muted-foreground hover:text-foreground"
                                            onClick={() =>
                                                toggleTokenVisibility(token.id)
                                            }
                                            title={
                                                visibleTokens[token.id]
                                                    ? 'Hide Token'
                                                    : 'Show Token'
                                            }
                                        >
                                            {visibleTokens[token.id] ? (
                                                <EyeOff className="h-4 w-4" />
                                            ) : (
                                                <Eye className="h-4 w-4" />
                                            )}
                                        </Button>
                                        <Button
                                            size="icon"
                                            variant="ghost"
                                            className="h-7 w-7 text-primary"
                                            onClick={() =>
                                                copyToClipboard(
                                                    token.token,
                                                    token.id,
                                                )
                                            }
                                            title="Copy Token"
                                        >
                                            {copiedTokenId === token.id ? (
                                                <Check className="h-4 w-4 text-emerald-500" />
                                            ) : (
                                                <Copy className="h-4 w-4" />
                                            )}
                                        </Button>
                                    </div>

                                    {/* Actions */}
                                    <div className="flex items-center justify-between border-t border-border/50 pt-1 text-[11px] text-muted-foreground">
                                        <span className="flex items-center gap-1">
                                            <Calendar className="h-3 w-3" />{' '}
                                            Created:{' '}
                                            {formatDate(token.created_at)}
                                        </span>
                                        <div className="flex items-center gap-3">
                                            <button
                                                onClick={() =>
                                                    handleToggleStatus(token.id)
                                                }
                                                className={`flex items-center gap-1 font-semibold hover:underline ${token.active ? 'text-amber-500' : 'text-emerald-500'}`}
                                            >
                                                <Power className="h-3 w-3" />{' '}
                                                {token.active
                                                    ? 'Disable'
                                                    : 'Enable'}
                                            </button>
                                            <span className="text-border">
                                                |
                                            </span>
                                            <button
                                                onClick={() =>
                                                    handleRegenerateToken(
                                                        token.id,
                                                        token.name,
                                                    )
                                                }
                                                className="flex items-center gap-1 font-semibold text-primary hover:underline"
                                            >
                                                <RefreshCw className="h-3 w-3" />{' '}
                                                Rotate Key
                                            </button>
                                            <span className="text-border">
                                                |
                                            </span>
                                            <button
                                                onClick={() =>
                                                    handleRevokeToken(
                                                        token.id,
                                                        token.name,
                                                    )
                                                }
                                                className="flex items-center gap-1 font-semibold text-destructive hover:underline"
                                            >
                                                <Trash2 className="h-3 w-3" />{' '}
                                                Revoke Key
                                            </button>
                                        </div>
                                    </div>
                                </div>
                            ))}
                        </div>
                    )}
                </div>
            </div>

            {/* Create API Token Dialog */}
            <Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
                <DialogContent className="border border-border bg-card text-card-foreground sm:max-w-md">
                    <DialogHeader>
                        <DialogTitle>Generate Developer API Key</DialogTitle>
                        <DialogDescription>
                            Create a secure token for accessing currency
                            resources. Leave base currency blank to use the
                            system default (INR).
                        </DialogDescription>
                    </DialogHeader>
                    <form onSubmit={handleCreateToken}>
                        <div className="space-y-4 py-4">
                            <div className="space-y-2">
                                <label
                                    htmlFor="name"
                                    className="text-xs font-semibold tracking-wider text-muted-foreground uppercase"
                                >
                                    Token Label / Name
                                </label>
                                <input
                                    id="name"
                                    type="text"
                                    placeholder="e.g. Production Application, Local Test Server"
                                    value={data.name}
                                    onChange={(e) =>
                                        setData('name', e.target.value)
                                    }
                                    required
                                    className="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm text-foreground shadow-xs transition-colors focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden"
                                />
                                {errors.name && (
                                    <p className="text-xs text-destructive">
                                        {errors.name}
                                    </p>
                                )}
                            </div>

                            <div className="space-y-2">
                                <label
                                    htmlFor="default_currency"
                                    className="text-xs font-semibold tracking-wider text-muted-foreground uppercase"
                                >
                                    Default Base Currency
                                </label>
                                <Combobox
                                    options={currencyOptions}
                                    value={data.default_currency}
                                    onChange={(val) =>
                                        setData('default_currency', val)
                                    }
                                    placeholder="Search base currency..."
                                />
                                {errors.default_currency && (
                                    <p className="text-xs text-destructive">
                                        {errors.default_currency}
                                    </p>
                                )}
                            </div>
                        </div>
                        <DialogFooter className="flex gap-2">
                            <Button
                                type="button"
                                variant="outline"
                                onClick={() => setIsCreateOpen(false)}
                            >
                                Cancel
                            </Button>
                            <Button
                                type="submit"
                                disabled={processing}
                                className="bg-primary text-primary-foreground shadow-md hover:bg-primary/90"
                            >
                                {processing
                                    ? 'Generating...'
                                    : 'Generate API Key'}
                            </Button>
                        </DialogFooter>
                    </form>
                </DialogContent>
            </Dialog>
        </>
    );
}

TokensPage.layout = {
    breadcrumbs: [
        {
            title: 'Dashboard',
            href: '/dashboard',
        },
        {
            title: 'API Tokens',
            href: '/dashboard/tokens',
        },
    ],
};