Update: Neue Seiten und Komponenten hinzugefügt

This commit is contained in:
2026-01-26 23:38:07 +01:00
parent d0066d3974
commit 75aef1941e
9 changed files with 563 additions and 208 deletions

View File

@@ -0,0 +1,8 @@
# Platform-Logos
Füge hier Logo-Dateien ein (PNG oder SVG, transparent, möglichst hohe Auflösung):
- `ebay.png` eBay-Logo („dickes“ Wortmarken-Logo)
- `amazon.png` Amazon-Logo
Ohne lokale Dateien werden Fallback-Logos (Wikimedia Commons) genutzt.

View File

@@ -3,7 +3,9 @@ import React, { useEffect, useState } from "react";
import { Sidebar, SidebarBody, SidebarLink } from "./components/sidebar";
import {
IconArrowLeft,
IconBan,
IconBrandTabler,
IconChartLine,
IconSettings,
IconUserBolt,
IconShoppingBag,
@@ -13,6 +15,9 @@ import { cn } from "./lib/utils";
import { BackgroundRippleEffect } from "./components/layout/BackgroundRippleEffect";
import { Dashboard } from "./components/dashboard/Dashboard";
import { AccountsPage } from "./pages/AccountsPage";
import { ItemsPage } from "./pages/ItemsPage";
import { BlacklistPage } from "./pages/BlacklistPage";
import { AnalysisPage } from "./pages/AnalysisPage";
import LogoutButton from "./components/ui/LogoutButton";
import { OnboardingGate } from "./components/onboarding/OnboardingGate";
import { SidebarHeader } from "./components/sidebar/SidebarHeader";
@@ -390,11 +395,22 @@ export default function App() {
navigate("/");
},
},
{
label: "Analysis",
href: "#/analysis",
icon: (
<IconChartLine className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
navigate("/analysis");
},
},
{
label: "Accounts",
href: "#/accounts",
icon: (
<IconShoppingBag className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
<IconUserBolt className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
disabled: scanning,
onClick: (e) => {
@@ -403,11 +419,26 @@ export default function App() {
},
},
{
label: "Profile",
href: "#",
label: "Items",
href: "#/items",
icon: (
<IconUserBolt className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
<IconShoppingBag className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
navigate("/items");
},
},
{
label: "Blacklist",
href: "#/blacklist",
icon: (
<IconBan className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
navigate("/blacklist");
},
},
{
label: "Settings",
@@ -416,17 +447,6 @@ export default function App() {
<IconSettings className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
},
{
label: "Logout",
href: "#",
icon: (
<IconArrowLeft className="h-5 w-5 shrink-0 text-neutral-700 dark:text-neutral-200" />
),
onClick: (e) => {
e.preventDefault();
logout();
},
},
];
// Rendere Content basierend auf Route
@@ -434,6 +454,15 @@ export default function App() {
if (route === "/accounts") {
return <AccountsPage />;
}
if (route === "/items") {
return <ItemsPage />;
}
if (route === "/blacklist") {
return <BlacklistPage />;
}
if (route === "/analysis") {
return <AnalysisPage />;
}
// Default: Dashboard
return <Dashboard />;
};

View File

@@ -0,0 +1,129 @@
"use client";
import React from "react";
import { useScrollSnap } from "./dashboard/hooks/useScrollSnap";
import { cn } from "../lib/utils";
const SECTION_IDS = ["s1", "s2", "s3", "s4"];
const SECTIONS = [
{ id: "s1", label: "Overview" },
{ id: "s2", label: "Accounts" },
{ id: "s3", label: "Products" },
{ id: "s4", label: "Page 4" },
];
function DummySection({ sectionId, title, pageTitle, onJumpToSection, activeSection }) {
return (
<section
id={sectionId}
className="flex min-h-screen w-auto flex-col gap-[18px] rounded-2xl px-4 py-4"
style={{
scrollSnapAlign: "start",
scrollSnapStop: "normal",
color: "var(--text)",
background: "transparent",
}}
>
<div className="flex items-center justify-between gap-4">
<div className="flex items-baseline gap-2.5">
<h1 className="m-0 text-lg font-medium tracking-wide text-[var(--text)]">{pageTitle}</h1>
<span className="rounded-full border border-[var(--line)] bg-white/3 px-2.5 py-1.5 text-xs text-[var(--muted)]">
snap page
</span>
</div>
<div className="flex flex-wrap items-center justify-end gap-2.5">
{SECTIONS.map(({ id, label }) => (
<button
key={id}
onClick={() => onJumpToSection(id)}
className={cn(
"rounded-xl px-3 py-2.5 text-xs transition-all active:translate-y-[1px]",
activeSection === id
? "border border-[rgba(106,166,255,0.7)] bg-[rgba(106,166,255,0.12)] text-[var(--text)] hover:border-[rgba(106,166,255,0.55)] hover:bg-[rgba(106,166,255,0.08)]"
: "border 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)]"
)}
>
{label}
</button>
))}
</div>
</div>
<div className="flex items-end justify-between gap-4">
<div>
<h2 className="m-0 text-xl tracking-wide text-[var(--text)]">{title}</h2>
<p className="mt-1.5 mb-0 text-xs text-[var(--muted)]">
Dummy section smooth scroll categories.
</p>
</div>
</div>
<div className="relative overflow-hidden rounded-[18px] border border-[var(--line)] bg-gradient-to-b from-white/4 to-white/2 p-6 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">
<p className="text-sm text-[var(--muted)]">
This is <strong className="text-[var(--text)]">{title}</strong>. Use the buttons above to jump between Overview, Accounts, Products, and Page 4.
</p>
</div>
</div>
</section>
);
}
export function ScrollSnapDummyPage({ pageTitle }) {
const { scrollToSection, activeSection } = useScrollSnap(SECTION_IDS);
const handleJumpToSection = (sectionId) => {
scrollToSection(sectionId);
};
return (
<div className="flex flex-1">
<div className="flex h-full w-full flex-1 flex-col gap-2 rounded-2xl border border-neutral-200 bg-white p-2 md:p-4 dark:border-neutral-700 dark:bg-neutral-900">
<div
className="h-full w-full overflow-y-scroll hide-scrollbar"
style={{
scrollSnapType: "y mandatory",
scrollBehavior: "smooth",
}}
>
<div className="min-h-screen">
<DummySection
sectionId="s1"
title="Overview"
pageTitle={pageTitle}
onJumpToSection={handleJumpToSection}
activeSection={activeSection}
/>
<DummySection
sectionId="s2"
title="Accounts"
pageTitle={pageTitle}
onJumpToSection={handleJumpToSection}
activeSection={activeSection}
/>
<DummySection
sectionId="s3"
title="Products"
pageTitle={pageTitle}
onJumpToSection={handleJumpToSection}
activeSection={activeSection}
/>
<DummySection
sectionId="s4"
title="Page 4"
pageTitle={pageTitle}
onJumpToSection={handleJumpToSection}
activeSection={activeSection}
/>
</div>
</div>
</div>
</div>
);
}

View File

@@ -5,10 +5,8 @@ export const useScrollSnap = (sectionIds) => {
const containerRef = useRef(null);
useEffect(() => {
const container = containerRef.current;
if (!container) return;
const sections = sectionIds.map(id => document.getElementById(id)).filter(Boolean);
if (sections.length === 0) return;
const observer = new IntersectionObserver(
(entries) => {

View File

@@ -0,0 +1,43 @@
"use client";
import { cn } from "../../lib/utils";
export const BentoGrid = ({ className, children }) => {
return (
<div
className={cn(
"mx-auto grid max-w-7xl grid-cols-1 gap-4 md:auto-rows-[18rem] md:grid-cols-3",
className
)}
>
{children}
</div>
);
};
export const BentoGridItem = ({
className,
title,
description,
header,
icon,
}) => {
return (
<div
className={cn(
"group/bento row-span-1 flex flex-col justify-between space-y-4 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",
className
)}
>
{header}
<div className="transition duration-200 group-hover/bento:translate-x-2">
{icon}
<div className="mt-2 mb-2 font-sans font-bold text-neutral-600 dark:text-neutral-200">
{title}
</div>
<div className="font-sans text-xs font-normal text-neutral-600 dark:text-neutral-300">
{description}
</div>
</div>
</div>
);
};

View File

@@ -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>
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" />
);
}
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 "-";
};
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">
{/* 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>
) : (
<DataTable columns={columns} data={accounts} renderCell={renderCell} />
)}
) : !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 */}

View File

@@ -0,0 +1,7 @@
"use client";
import React from "react";
import { ScrollSnapDummyPage } from "../components/ScrollSnapDummyPage";
export const AnalysisPage = () => {
return <ScrollSnapDummyPage pageTitle="Analysis" />;
};

View File

@@ -0,0 +1,7 @@
"use client";
import React from "react";
import { ScrollSnapDummyPage } from "../components/ScrollSnapDummyPage";
export const BlacklistPage = () => {
return <ScrollSnapDummyPage pageTitle="Blacklist" />;
};

View File

@@ -0,0 +1,7 @@
"use client";
import React from "react";
import { ScrollSnapDummyPage } from "../components/ScrollSnapDummyPage";
export const ItemsPage = () => {
return <ScrollSnapDummyPage pageTitle="Items" />;
};