diff --git a/Server/public/assets/platforms/README.md b/Server/public/assets/platforms/README.md
new file mode 100644
index 0000000..6f028ea
--- /dev/null
+++ b/Server/public/assets/platforms/README.md
@@ -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.
diff --git a/Server/src/App.jsx b/Server/src/App.jsx
index 141aa85..11b7722 100644
--- a/Server/src/App.jsx
+++ b/Server/src/App.jsx
@@ -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: (
+
+ ),
+ onClick: (e) => {
+ e.preventDefault();
+ navigate("/analysis");
+ },
+ },
{
label: "Accounts",
href: "#/accounts",
icon: (
-
+
),
disabled: scanning,
onClick: (e) => {
@@ -403,11 +419,26 @@ export default function App() {
},
},
{
- label: "Profile",
- href: "#",
+ label: "Items",
+ href: "#/items",
icon: (
-
+
),
+ onClick: (e) => {
+ e.preventDefault();
+ navigate("/items");
+ },
+ },
+ {
+ label: "Blacklist",
+ href: "#/blacklist",
+ icon: (
+
+ ),
+ onClick: (e) => {
+ e.preventDefault();
+ navigate("/blacklist");
+ },
},
{
label: "Settings",
@@ -416,17 +447,6 @@ export default function App() {
),
},
- {
- label: "Logout",
- href: "#",
- icon: (
-
- ),
- onClick: (e) => {
- e.preventDefault();
- logout();
- },
- },
];
// Rendere Content basierend auf Route
@@ -434,6 +454,15 @@ export default function App() {
if (route === "/accounts") {
return ;
}
+ if (route === "/items") {
+ return ;
+ }
+ if (route === "/blacklist") {
+ return ;
+ }
+ if (route === "/analysis") {
+ return ;
+ }
// Default: Dashboard
return ;
};
diff --git a/Server/src/components/ScrollSnapDummyPage.jsx b/Server/src/components/ScrollSnapDummyPage.jsx
new file mode 100644
index 0000000..105777c
--- /dev/null
+++ b/Server/src/components/ScrollSnapDummyPage.jsx
@@ -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 (
+
+
+
+
{pageTitle}
+
+ snap page
+
+
+
+ {SECTIONS.map(({ id, label }) => (
+ 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}
+
+ ))}
+
+
+
+
+
+
{title}
+
+ Dummy section – smooth scroll categories.
+
+
+
+
+
+
+
+
+ This is {title} . Use the buttons above to jump between Overview, Accounts, Products, and Page 4.
+
+
+
+
+ );
+}
+
+export function ScrollSnapDummyPage({ pageTitle }) {
+ const { scrollToSection, activeSection } = useScrollSnap(SECTION_IDS);
+
+ const handleJumpToSection = (sectionId) => {
+ scrollToSection(sectionId);
+ };
+
+ return (
+
+ );
+}
diff --git a/Server/src/components/dashboard/hooks/useScrollSnap.js b/Server/src/components/dashboard/hooks/useScrollSnap.js
index 3e0730d..fb0d3fe 100644
--- a/Server/src/components/dashboard/hooks/useScrollSnap.js
+++ b/Server/src/components/dashboard/hooks/useScrollSnap.js
@@ -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) => {
diff --git a/Server/src/components/ui/bento-grid.jsx b/Server/src/components/ui/bento-grid.jsx
new file mode 100644
index 0000000..ef20803
--- /dev/null
+++ b/Server/src/components/ui/bento-grid.jsx
@@ -0,0 +1,43 @@
+"use client";
+import { cn } from "../../lib/utils";
+
+export const BentoGrid = ({ className, children }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export const BentoGridItem = ({
+ className,
+ title,
+ description,
+ header,
+ icon,
+}) => {
+ return (
+
+ {header}
+
+ {icon}
+
+ {title}
+
+
+ {description}
+
+
+
+ );
+};
diff --git a/Server/src/pages/AccountsPage.jsx b/Server/src/pages/AccountsPage.jsx
index 6ac08d1..313c5f0 100644
--- a/Server/src/pages/AccountsPage.jsx
+++ b/Server/src/pages/AccountsPage.jsx
@@ -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 (
+
+
+
+ {name}
+
+ {url ? (
+
+ {name}
+
+ ) : (
+
+ {name}
+
+ )}
+
+ {platformAccountId != null && platformAccountId !== "" && (
+
+ ID: {platformAccountId}
+
+ )}
+
+
Account wechseln
+
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"
+ >
+ Account wählen…
+
+
+ {listOpen && (
+
+ {otherAccounts.length === 0 ? (
+ Keine weiteren Accounts
+ ) : (
+ otherAccounts.map((acc) => {
+ const id = acc.$id || acc.id;
+ const label = getAccountDisplayName(acc) || id;
+ return (
+
+ {
+ 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}
+
+
+ );
+ })
+ )}
+
+ )}
+
+
+ );
+}
+
+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 (
+
+ {cfg ? (
+
+ ) : (
+
{platform || "–"}
+ )}
+ {market != null && String(market).trim() !== "" && (
+
+ {String(market).trim().toUpperCase()}
+
+ )}
+
+ );
+}
+
+function RangCard({ rank, className }) {
+ return (
+
+ );
+}
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 (
-
- 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"
- >
-
- Refresh
-
- 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
-
-
- );
- }
-
- 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 (
-
- {url.length > 40 ? `${url.substring(0, 40)}...` : url}
-
- );
- }
-
- 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 = () => (
+
+ );
return (
@@ -359,72 +499,6 @@ export const AccountsPage = () => {
- {/* Hilfe-Panel */}
-
-
-
-
- Account hinzufügen
-
-
-
-
- eBay Account URL (Pflichtfeld)
-
- : Gib einfach die eBay-URL zum Verkäuferprofil oder Shop ein. Alle weiteren Informationen (Market, Seller ID, Shop Name) werden automatisch erkannt.
-
-
-
- Market (Auto)
-
- : Wird automatisch aus der URL extrahiert (z.B. DE, US, UK). Du musst nichts eingeben.
-
-
-
- eBay Seller ID (Auto)
-
- : Wird automatisch erkannt. Dies ist die eindeutige Verkäufer-Kennung von eBay und verhindert Duplikate.
-
-
-
- Shop Name (Auto)
-
- : Öffentlich sichtbarer Name des Shops. Wird automatisch aus der URL/Seite extrahiert.
-
-
-
- Sales (Auto)
-
- : Anzahl der verkauften Artikel wird automatisch aus dem eBay-Profil gelesen.
-
-
-
- So funktioniert's: {" "}
- 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.
-
-
- {/* Advanced Toggle */}
-
setShowAdvanced(!showAdvanced)}
- className="mt-4 flex items-center gap-2 text-xs text-[var(--muted)] transition-colors hover:text-[var(--text)]"
- >
-
- {showAdvanced ? "Weniger anzeigen" : "Erweitert anzeigen"}
-
-
-
-
{/* Toast Notification */}
{refreshToast.show && (
@@ -444,24 +518,77 @@ export const AccountsPage = () => {
)}
- {/* Tabelle */}
-
-
-
- {loading ? (
-
- Loading accounts...
-
- ) : (
-
- )}
-
+ {/* Bento Grid – nur ein Account */}
+
+ {loading ? (
+
+ Loading accounts...
+
+ ) : !displayedAccount ? (
+
+ No accounts yet. Add one above.
+
+ ) : (
+ (() => {
+ 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 (
+
+
+
+
+ }
+ icon={ }
+ className="md:col-span-2"
+ />
+
+ 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"
+ >
+
+ Refresh
+
+
+ }
+ header={
}
+ icon={
}
+ />
+
+ );
+ })()
+ )}
{/* Add Account Form Modal */}
diff --git a/Server/src/pages/AnalysisPage.jsx b/Server/src/pages/AnalysisPage.jsx
new file mode 100644
index 0000000..fcf06b0
--- /dev/null
+++ b/Server/src/pages/AnalysisPage.jsx
@@ -0,0 +1,7 @@
+"use client";
+import React from "react";
+import { ScrollSnapDummyPage } from "../components/ScrollSnapDummyPage";
+
+export const AnalysisPage = () => {
+ return ;
+};
diff --git a/Server/src/pages/BlacklistPage.jsx b/Server/src/pages/BlacklistPage.jsx
new file mode 100644
index 0000000..99d4bf8
--- /dev/null
+++ b/Server/src/pages/BlacklistPage.jsx
@@ -0,0 +1,7 @@
+"use client";
+import React from "react";
+import { ScrollSnapDummyPage } from "../components/ScrollSnapDummyPage";
+
+export const BlacklistPage = () => {
+ return ;
+};
diff --git a/Server/src/pages/ItemsPage.jsx b/Server/src/pages/ItemsPage.jsx
new file mode 100644
index 0000000..f829548
--- /dev/null
+++ b/Server/src/pages/ItemsPage.jsx
@@ -0,0 +1,7 @@
+"use client";
+import React from "react";
+import { ScrollSnapDummyPage } from "../components/ScrollSnapDummyPage";
+
+export const ItemsPage = () => {
+ return ;
+};