Update: Neue Seiten und Komponenten hinzugefügt
This commit is contained in:
@@ -1,23 +1,241 @@
|
||||
"use client";
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { IconPlus, IconChevronDown, IconX, IconRefresh } from "@tabler/icons-react";
|
||||
import {
|
||||
IconPlus,
|
||||
IconX,
|
||||
IconRefresh,
|
||||
IconChevronDown,
|
||||
IconChartBar,
|
||||
IconSettings,
|
||||
} from "@tabler/icons-react";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { cn } from "../lib/utils";
|
||||
import { useHashRoute } from "../lib/routing";
|
||||
import {
|
||||
setActiveAccountId,
|
||||
getActiveAccountId,
|
||||
getAccountDisplayName,
|
||||
} from "../services/accountService";
|
||||
import { fetchManagedAccounts, createManagedAccount, updateManagedAccount } from "../services/accountsService";
|
||||
import { getAuthUser } from "../lib/appwrite";
|
||||
import { parseEbayAccount } from "../services/ebayParserService";
|
||||
import { DataTable } from "../components/dashboard/ui/DataTable";
|
||||
import { BentoGrid, BentoGridItem } from "../components/ui/bento-grid";
|
||||
|
||||
function AccountNameCard({
|
||||
name,
|
||||
url,
|
||||
platformAccountId,
|
||||
accounts,
|
||||
displayedAccountId,
|
||||
onSelectAccount,
|
||||
className,
|
||||
}) {
|
||||
const containerRef = React.useRef(null);
|
||||
const measureRef = React.useRef(null);
|
||||
const listRef = React.useRef(null);
|
||||
const [fontSize, setFontSize] = React.useState(48);
|
||||
const [listOpen, setListOpen] = React.useState(false);
|
||||
|
||||
const otherAccounts = React.useMemo(
|
||||
() => accounts.filter((acc) => (acc.$id || acc.id) !== displayedAccountId),
|
||||
[accounts, displayedAccountId]
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!listOpen) return;
|
||||
const handleClickOutside = (e) => {
|
||||
if (listRef.current && !listRef.current.contains(e.target)) setListOpen(false);
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||
}, [listOpen]);
|
||||
|
||||
React.useEffect(() => {
|
||||
const cont = containerRef.current;
|
||||
const meas = measureRef.current;
|
||||
if (!cont || !meas || !name) return;
|
||||
|
||||
const fit = () => {
|
||||
const w = cont.clientWidth;
|
||||
if (w <= 0) return;
|
||||
let fs = 48;
|
||||
meas.style.fontSize = `${fs}px`;
|
||||
while (meas.scrollWidth > w && fs > 12) {
|
||||
fs -= 2;
|
||||
meas.style.fontSize = `${fs}px`;
|
||||
}
|
||||
setFontSize(fs);
|
||||
};
|
||||
|
||||
fit();
|
||||
const ro = new ResizeObserver(fit);
|
||||
ro.observe(cont);
|
||||
return () => ro.disconnect();
|
||||
}, [name]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"row-span-1 flex h-full min-h-[12rem] flex-col rounded-xl border border-neutral-200 bg-white p-4 shadow-md transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-neutral-900 dark:shadow-none md:min-h-[18rem]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="relative flex w-full shrink-0 flex-col items-start justify-start overflow-hidden text-left"
|
||||
>
|
||||
<span
|
||||
ref={measureRef}
|
||||
className="pointer-events-none invisible absolute left-0 top-0 whitespace-nowrap font-bold leading-tight text-[var(--text)]"
|
||||
aria-hidden
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
{url ? (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="max-w-full shrink-0 whitespace-nowrap font-bold leading-tight text-[var(--text)] hover:underline"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
>
|
||||
{name}
|
||||
</a>
|
||||
) : (
|
||||
<span
|
||||
className="max-w-full shrink-0 whitespace-nowrap font-bold leading-tight text-[var(--text)]"
|
||||
style={{ fontSize: `${fontSize}px` }}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{platformAccountId != null && platformAccountId !== "" && (
|
||||
<div className="mt-1 shrink-0 text-sm text-[var(--muted)]">
|
||||
ID: {platformAccountId}
|
||||
</div>
|
||||
)}
|
||||
<div ref={listRef} className="relative mt-2 flex shrink-0 flex-col gap-1">
|
||||
<label className="text-xs font-medium text-[var(--muted)]">Account wechseln</label>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setListOpen((o) => !o)}
|
||||
className="flex w-full items-center justify-between rounded-xl border border-[var(--line)] bg-white/5 px-3 py-2 text-left text-sm text-[var(--text)] outline-none transition-colors hover:border-[rgba(106,166,255,0.4)] focus:border-[rgba(106,166,255,0.5)] dark:bg-neutral-800"
|
||||
>
|
||||
<span>Account wählen…</span>
|
||||
<IconChevronDown className={cn("h-4 w-4 shrink-0 transition-transform", listOpen && "rotate-180")} />
|
||||
</button>
|
||||
{listOpen && (
|
||||
<ul className="absolute left-0 right-0 top-full z-10 mt-1 max-h-48 overflow-y-auto rounded-xl border border-[var(--line)] bg-white shadow-lg dark:bg-neutral-800">
|
||||
{otherAccounts.length === 0 ? (
|
||||
<li className="px-3 py-2 text-xs text-[var(--muted)]">Keine weiteren Accounts</li>
|
||||
) : (
|
||||
otherAccounts.map((acc) => {
|
||||
const id = acc.$id || acc.id;
|
||||
const label = getAccountDisplayName(acc) || id;
|
||||
return (
|
||||
<li key={id}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
onSelectAccount(id);
|
||||
setListOpen(false);
|
||||
}}
|
||||
className="flex w-full items-center px-3 py-2 text-left text-sm text-[var(--text)] transition-colors hover:bg-white/10 dark:hover:bg-neutral-700"
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
</li>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const PLATFORM_LOGOS = {
|
||||
ebay: {
|
||||
local: "/assets/platforms/ebay.png",
|
||||
fallback: "https://upload.wikimedia.org/wikipedia/commons/1/1b/EBay_logo.svg",
|
||||
},
|
||||
amazon: {
|
||||
local: "/assets/platforms/amazon.png",
|
||||
fallback: "https://upload.wikimedia.org/wikipedia/commons/a/a9/Amazon_logo.svg",
|
||||
},
|
||||
};
|
||||
|
||||
function PlatformLogoCard({ platform, market, className }) {
|
||||
const key = (platform || "").toLowerCase();
|
||||
const cfg = PLATFORM_LOGOS[key];
|
||||
const [src, setSrc] = React.useState(cfg ? cfg.local : null);
|
||||
const [usedFallback, setUsedFallback] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const c = PLATFORM_LOGOS[key];
|
||||
if (!c) {
|
||||
setSrc(null);
|
||||
setUsedFallback(true);
|
||||
return;
|
||||
}
|
||||
setSrc(c.local);
|
||||
setUsedFallback(false);
|
||||
}, [key]);
|
||||
|
||||
const onError = React.useCallback(() => {
|
||||
if (usedFallback || !cfg) return;
|
||||
setSrc(cfg.fallback);
|
||||
setUsedFallback(true);
|
||||
}, [cfg, usedFallback]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"row-span-1 relative flex h-full min-h-[12rem] items-center justify-center overflow-hidden rounded-xl border border-neutral-200 bg-white p-4 shadow-md transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-neutral-900 dark:shadow-none md:min-h-[18rem]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
{cfg ? (
|
||||
<img
|
||||
src={src || cfg.fallback}
|
||||
alt=""
|
||||
onError={onError}
|
||||
className="max-h-full w-full object-contain"
|
||||
/>
|
||||
) : (
|
||||
<span className="text-xs text-[var(--muted)]">{platform || "–"}</span>
|
||||
)}
|
||||
{market != null && String(market).trim() !== "" && (
|
||||
<div className="absolute bottom-2 right-2 font-bold text-[var(--text)]">
|
||||
{String(market).trim().toUpperCase()}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RangCard({ rank, className }) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"row-span-1 flex h-full min-h-[12rem] flex-col items-center justify-center rounded-xl border border-neutral-200 bg-white p-4 shadow-md transition duration-200 hover:shadow-xl dark:border-white/[0.2] dark:bg-neutral-900 dark:shadow-none md:min-h-[18rem]",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<div className="text-center">
|
||||
<div className="text-xs font-medium uppercase tracking-wide text-[var(--muted)]">Rang</div>
|
||||
<div className="mt-1 text-2xl font-bold text-[var(--text)]">{rank ?? "–"}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const AccountsPage = () => {
|
||||
const { navigate } = useHashRoute();
|
||||
const [accounts, setAccounts] = useState([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [showAdvanced, setShowAdvanced] = useState(false);
|
||||
const [showAddForm, setShowAddForm] = useState(false);
|
||||
const [formError, setFormError] = useState("");
|
||||
const [formSuccess, setFormSuccess] = useState("");
|
||||
@@ -32,6 +250,9 @@ export const AccountsPage = () => {
|
||||
const [refreshingAccountId, setRefreshingAccountId] = useState(null);
|
||||
const [refreshToast, setRefreshToast] = useState({ show: false, message: "", type: "success" });
|
||||
|
||||
// Nur ein Account wird angezeigt; Wechsel über Dropdown
|
||||
const [displayedAccountId, setDisplayedAccountId] = useState(null);
|
||||
|
||||
// Form-Felder (nur noch URL)
|
||||
const [formData, setFormData] = useState({
|
||||
account_url: "",
|
||||
@@ -42,6 +263,21 @@ export const AccountsPage = () => {
|
||||
loadAccounts();
|
||||
}, []);
|
||||
|
||||
// displayedAccountId setzen sobald Accounts geladen (aktiv oder erster)
|
||||
useEffect(() => {
|
||||
if (accounts.length === 0) {
|
||||
setDisplayedAccountId(null);
|
||||
return;
|
||||
}
|
||||
const active = getActiveAccountId();
|
||||
const hasActive = accounts.some((a) => (a.$id || a.id) === active);
|
||||
if (hasActive) {
|
||||
setDisplayedAccountId(active);
|
||||
} else {
|
||||
setDisplayedAccountId(accounts[0].$id || accounts[0].id);
|
||||
}
|
||||
}, [accounts]);
|
||||
|
||||
async function loadAccounts() {
|
||||
setLoading(true);
|
||||
try {
|
||||
@@ -61,11 +297,9 @@ export const AccountsPage = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const handleSelectAccount = (account) => {
|
||||
const accountId = account.$id || account.id;
|
||||
const handleDisplayedAccountChange = (accountId) => {
|
||||
setDisplayedAccountId(accountId);
|
||||
setActiveAccountId(accountId);
|
||||
// Navigiere zurück zum Dashboard
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
const handleRefreshAccount = async (account) => {
|
||||
@@ -236,106 +470,12 @@ export const AccountsPage = () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Spalten für die Tabelle
|
||||
const columns = [
|
||||
"Account Name",
|
||||
"Platform",
|
||||
"Platform Account ID",
|
||||
"Market",
|
||||
"Account URL",
|
||||
"Sales",
|
||||
"Last Scan",
|
||||
...(showAdvanced ? ["Owner User ID"] : []),
|
||||
"Action",
|
||||
];
|
||||
const displayedAccount =
|
||||
accounts.find((a) => (a.$id || a.id) === displayedAccountId) ?? accounts[0] ?? null;
|
||||
|
||||
const renderCell = (col, row) => {
|
||||
if (col === "Action") {
|
||||
const accountId = row.$id || row.id;
|
||||
const isRefreshing = refreshingAccountId === accountId;
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleRefreshAccount(row)}
|
||||
disabled={isRefreshing}
|
||||
className="rounded-xl border border-[var(--line)] bg-white/3 px-3 py-2 text-xs text-[var(--text)] transition-all hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)] active:translate-y-[1px] disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1.5"
|
||||
title="Account aktualisieren"
|
||||
>
|
||||
<IconRefresh className={cn("h-3.5 w-3.5", isRefreshing && "animate-spin")} />
|
||||
Refresh
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleSelectAccount(row)}
|
||||
className="rounded-xl border border-[var(--line)] bg-white/3 px-3 py-2 text-xs text-[var(--text)] transition-all hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)] active:translate-y-[1px]"
|
||||
>
|
||||
Select
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (col === "Account Name") {
|
||||
return getAccountDisplayName(row) || "-";
|
||||
}
|
||||
|
||||
if (col === "Platform") {
|
||||
return row.account_platform || "-";
|
||||
}
|
||||
|
||||
if (col === "Platform Account ID") {
|
||||
return row.account_platform_account_id || "-";
|
||||
}
|
||||
|
||||
if (col === "Market") {
|
||||
return row.account_platform_market || "-";
|
||||
}
|
||||
|
||||
if (col === "Account URL") {
|
||||
const url = row.account_url;
|
||||
if (!url) return "-";
|
||||
return (
|
||||
<a
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-blue-500 hover:underline dark:text-blue-400"
|
||||
>
|
||||
{url.length > 40 ? `${url.substring(0, 40)}...` : url}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
if (col === "Sales") {
|
||||
const sales = row.account_sells;
|
||||
if (sales === null || sales === undefined) return "-";
|
||||
// Format number with thousand separators
|
||||
return new Intl.NumberFormat("de-DE").format(sales);
|
||||
}
|
||||
|
||||
if (col === "Last Scan") {
|
||||
const lastScan = row.account_updated_at;
|
||||
if (!lastScan) return "-";
|
||||
try {
|
||||
const date = new Date(lastScan);
|
||||
return date.toLocaleString("de-DE", {
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
} catch (e) {
|
||||
return "-";
|
||||
}
|
||||
}
|
||||
|
||||
if (col === "Owner User ID") {
|
||||
return row.account_owner_user_id || "-";
|
||||
}
|
||||
|
||||
return "-";
|
||||
};
|
||||
const BentoHeader = () => (
|
||||
<div className="flex flex-1 w-full h-full min-h-[4rem] rounded-xl border border-neutral-100 dark:border-white/[0.2] bg-neutral-50 dark:bg-neutral-800" />
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-1">
|
||||
@@ -359,72 +499,6 @@ export const AccountsPage = () => {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Hilfe-Panel */}
|
||||
<div className="relative overflow-hidden rounded-[18px] border border-[var(--line)] bg-gradient-to-b from-white/4 to-white/2 p-4 shadow-[0_10px_30px_rgba(0,0,0,0.35)]">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-[-1px] opacity-55"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(600px 280px at 20% 0%, rgba(106,166,255,0.14), transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
<h2 className="mb-3 text-sm font-semibold text-[var(--text)]">
|
||||
Account hinzufügen
|
||||
</h2>
|
||||
<div className="grid gap-3 text-xs text-[var(--muted)]">
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text)]">
|
||||
eBay Account URL <span className="text-red-500">(Pflichtfeld)</span>
|
||||
</span>
|
||||
: Gib einfach die eBay-URL zum Verkäuferprofil oder Shop ein. Alle weiteren Informationen (Market, Seller ID, Shop Name) werden automatisch erkannt.
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text)]">
|
||||
Market (Auto)
|
||||
</span>
|
||||
: Wird automatisch aus der URL extrahiert (z.B. DE, US, UK). Du musst nichts eingeben.
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text)]">
|
||||
eBay Seller ID (Auto)
|
||||
</span>
|
||||
: Wird automatisch erkannt. Dies ist die eindeutige Verkäufer-Kennung von eBay und verhindert Duplikate.
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text)]">
|
||||
Shop Name (Auto)
|
||||
</span>
|
||||
: Öffentlich sichtbarer Name des Shops. Wird automatisch aus der URL/Seite extrahiert.
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-medium text-[var(--text)]">
|
||||
Sales (Auto)
|
||||
</span>
|
||||
: Anzahl der verkauften Artikel wird automatisch aus dem eBay-Profil gelesen.
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 rounded-lg border border-[var(--line)] bg-white/2 p-3 text-xs text-[var(--muted)]">
|
||||
<span className="font-medium text-[var(--text)]">So funktioniert's:</span>{" "}
|
||||
Gib einfach die eBay-URL ein und klicke auf "Account hinzufügen". Das System liest alle notwendigen Informationen automatisch aus. Du musst keine technischen Felder manuell ausfüllen.
|
||||
</div>
|
||||
|
||||
{/* Advanced Toggle */}
|
||||
<button
|
||||
onClick={() => setShowAdvanced(!showAdvanced)}
|
||||
className="mt-4 flex items-center gap-2 text-xs text-[var(--muted)] transition-colors hover:text-[var(--text)]"
|
||||
>
|
||||
<IconChevronDown
|
||||
className={cn(
|
||||
"h-4 w-4 transition-transform",
|
||||
showAdvanced && "rotate-180"
|
||||
)}
|
||||
/>
|
||||
{showAdvanced ? "Weniger anzeigen" : "Erweitert anzeigen"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Toast Notification */}
|
||||
<AnimatePresence>
|
||||
{refreshToast.show && (
|
||||
@@ -444,24 +518,77 @@ export const AccountsPage = () => {
|
||||
)}
|
||||
</AnimatePresence>
|
||||
|
||||
{/* Tabelle */}
|
||||
<div className="relative overflow-hidden rounded-[18px] border border-[var(--line)] bg-gradient-to-b from-white/4 to-white/2 p-4 shadow-[0_10px_30px_rgba(0,0,0,0.35)]">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-[-1px] opacity-55"
|
||||
style={{
|
||||
background:
|
||||
"radial-gradient(600px 280px at 20% 0%, rgba(106,166,255,0.14), transparent 60%)",
|
||||
}}
|
||||
/>
|
||||
<div className="relative">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-[var(--muted)]">
|
||||
Loading accounts...
|
||||
</div>
|
||||
) : (
|
||||
<DataTable columns={columns} data={accounts} renderCell={renderCell} />
|
||||
)}
|
||||
</div>
|
||||
{/* Bento Grid – nur ein Account */}
|
||||
<div className="flex flex-1 flex-col gap-8 overflow-y-auto">
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-[var(--muted)]">
|
||||
Loading accounts...
|
||||
</div>
|
||||
) : !displayedAccount ? (
|
||||
<div className="flex items-center justify-center py-12 text-sm text-[var(--muted)]">
|
||||
No accounts yet. Add one above.
|
||||
</div>
|
||||
) : (
|
||||
(() => {
|
||||
const account = displayedAccount;
|
||||
const accountId = account.$id || account.id;
|
||||
const isRefreshing = refreshingAccountId === accountId;
|
||||
const name = getAccountDisplayName(account) || "–";
|
||||
const url = account.account_url;
|
||||
const sales =
|
||||
account.account_sells != null
|
||||
? new Intl.NumberFormat("de-DE").format(account.account_sells)
|
||||
: "–";
|
||||
|
||||
return (
|
||||
<BentoGrid key={accountId} className="max-w-4xl mx-auto md:auto-rows-[18rem]">
|
||||
<AccountNameCard
|
||||
name={name}
|
||||
url={url}
|
||||
platformAccountId={account.account_platform_account_id}
|
||||
accounts={accounts}
|
||||
displayedAccountId={displayedAccountId}
|
||||
onSelectAccount={handleDisplayedAccountChange}
|
||||
className="md:col-span-1"
|
||||
/>
|
||||
<PlatformLogoCard
|
||||
platform={account.account_platform}
|
||||
market={account.account_platform_market}
|
||||
className="md:col-span-1"
|
||||
/>
|
||||
<RangCard rank={undefined} className="md:col-span-1" />
|
||||
<BentoGridItem
|
||||
title="Sales"
|
||||
description={sales}
|
||||
header={<BentoHeader />}
|
||||
icon={<IconChartBar className="h-4 w-4 text-neutral-500" />}
|
||||
className="md:col-span-2"
|
||||
/>
|
||||
<BentoGridItem
|
||||
title="Actions"
|
||||
description={
|
||||
<div className="flex flex-wrap items-center gap-2 mt-1">
|
||||
<button
|
||||
onClick={() => handleRefreshAccount(account)}
|
||||
disabled={isRefreshing}
|
||||
className={cn(
|
||||
"inline-flex items-center gap-1.5 rounded-xl border px-3 py-2 text-xs transition-all active:translate-y-[1px] disabled:opacity-50 disabled:cursor-not-allowed",
|
||||
"border-[var(--line)] bg-white/3 text-[var(--text)] hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)]"
|
||||
)}
|
||||
title="Account aktualisieren"
|
||||
>
|
||||
<IconRefresh className={cn("h-3.5 w-3.5", isRefreshing && "animate-spin")} />
|
||||
Refresh
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
header={<BentoHeader />}
|
||||
icon={<IconSettings className="h-4 w-4 text-neutral-500" />}
|
||||
/>
|
||||
</BentoGrid>
|
||||
);
|
||||
})()
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Add Account Form Modal */}
|
||||
|
||||
Reference in New Issue
Block a user