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 }) => ( + + ))} +
+
+ +
+
+

{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} +
+ )} +
+ + + {listOpen && ( +
    + {otherAccounts.length === 0 ? ( +
  • Keine weiteren Accounts
  • + ) : ( + otherAccounts.map((acc) => { + const id = acc.$id || acc.id; + const label = getAccountDisplayName(acc) || id; + return ( +
  • + +
  • + ); + }) + )} +
+ )} +
+
+ ); +} + +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 ( +
+
+
Rang
+
{rank ?? "–"}
+
+
+ ); +} 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 ( -
- - -
- ); - } - - 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 */} - -
-
- {/* 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" + /> + + +
+ } + 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 ; +};