From e1480526cc4bf8167f66e01153f7b1eecf56a101 Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:06:24 +0100 Subject: [PATCH 01/19] Redesign locations page with card-based grid layout Replaces the basic flex-wrap layout with a responsive CSS grid, proper card styling with borders and hover effects, weight progress bars on spool cards, count badges per location, and uses Refine's List wrapper for a consistent header. Empty locations now show a placeholder state. --- .../pages/locations/components/location.tsx | 41 ++++-- .../components/locationContainer.tsx | 43 +++--- .../pages/locations/components/spoolCard.tsx | 33 ++++- .../pages/locations/components/spoolList.tsx | 12 +- client/src/pages/locations/index.tsx | 8 +- client/src/pages/locations/locations.css | 134 ++++++++++++++++-- 6 files changed, 204 insertions(+), 67 deletions(-) diff --git a/client/src/pages/locations/components/location.tsx b/client/src/pages/locations/components/location.tsx index 0a90b44b4..64a855af4 100644 --- a/client/src/pages/locations/components/location.tsx +++ b/client/src/pages/locations/components/location.tsx @@ -3,7 +3,7 @@ import type { Identifier, XYCoord } from "dnd-core"; import { useRef, useState } from "react"; import { DragSourceMonitor, useDrag, useDrop } from "react-dnd"; -import { DeleteOutlined } from "@ant-design/icons"; +import { DeleteOutlined, InboxOutlined } from "@ant-design/icons"; import { useTranslate, useUpdate } from "@refinedev/core"; import { ISpool } from "../../spools/model"; import { DragItem, ItemTypes, SpoolDragItem } from "../dnd"; @@ -136,18 +136,15 @@ export function Location({ const canEditTitle = title != EMPTYLOC; - const titleStyle = { - color: canEditTitle ? undefined : token.colorTextTertiary, - }; - const spoolCountStyle = { - color: token.colorTextQuaternary, - }; - return (

@@ -162,6 +159,7 @@ export function Location({ setEditTitle(false); return onEditTitle(newTitle); }} + style={{ fontWeight: 600 }} /> ) : ( {displayTitle} - { ({spools.length})} + 0 ? token.colorPrimaryBg : token.colorBgContainerDisabled, + color: spools.length > 0 ? token.colorPrimaryText : token.colorTextQuaternary, + }} + > + {spools.length} + )} - {showDelete &&

- + {spools.length === 0 ? ( +
+ + {t("locations.no_locations_help") || "Drop spools here"} +
+ ) : ( + + )}
); } diff --git a/client/src/pages/locations/components/locationContainer.tsx b/client/src/pages/locations/components/locationContainer.tsx index 263bc75a2..726a875e0 100644 --- a/client/src/pages/locations/components/locationContainer.tsx +++ b/client/src/pages/locations/components/locationContainer.tsx @@ -1,6 +1,6 @@ import { PlusOutlined } from "@ant-design/icons"; import { useList, useTranslate } from "@refinedev/core"; -import { Button } from "antd"; +import { Button, Spin } from "antd"; import { useEffect, useMemo } from "react"; import { useSetSetting } from "../../../utils/querySettings"; import { ISpool } from "../../spools/model"; @@ -151,7 +151,11 @@ export function LocationContainer() { }, [locationsList, settingsLocations, setLocationsSetting]); if (isLoading) { - return
Loading...
; + return ( +
+ +
+ ); } if (isError) { @@ -173,26 +177,27 @@ export function LocationContainer() { setLocationsSetting.mutate(newLocs); }; + // Count totals + const totalSpools = spoolData?.data?.length ?? 0; + const totalLocations = locationsList.filter((l) => l !== EMPTYLOC).length; + return (
- {!isLoading && spoolData.data.length == 0 && ( -
{t("locations.no_locations_help")}
- )} -
- {containers} -
-
+
+ + {totalSpools} {t("spool.spool", { count: totalSpools })} in {totalLocations}{" "} + {t("locations.locations").toLowerCase()} + +
+ {!isLoading && totalSpools == 0 && ( +
+ {t("locations.no_locations_help")} +
+ )} +
{containers}
); } diff --git a/client/src/pages/locations/components/spoolCard.tsx b/client/src/pages/locations/components/spoolCard.tsx index b935636cd..4cbbe2461 100644 --- a/client/src/pages/locations/components/spoolCard.tsx +++ b/client/src/pages/locations/components/spoolCard.tsx @@ -19,6 +19,18 @@ dayjs.extend(relativeTime); const { useToken } = theme; +function getWeightPercentage(spool: ISpool): number { + const total = spool.initial_weight ?? spool.filament.weight ?? 1000; + const remaining = spool.remaining_weight ?? total; + return Math.max(0, Math.min(100, (remaining / total) * 100)); +} + +function getWeightColor(percentage: number): string { + if (percentage <= 10) return "#ff4d4f"; + if (percentage <= 25) return "#faad14"; + return "#52c41a"; +} + export function SpoolCard({ index, spool, @@ -134,6 +146,9 @@ export function SpoolCard({ filament_name = spool.filament.name ?? spool.filament.id.toString(); } + const weightPct = getWeightPercentage(spool); + const weightColor = getWeightColor(weightPct); + const opacity = draggedSpoolId === spool.id ? 0 : 1; const style = { opacity, @@ -143,15 +158,14 @@ export function SpoolCard({ function formatSubtitle(spool: ISpool) { let str = ""; - if (spool.filament.material) str += spool.filament.material + " - "; + if (spool.filament.material) str += spool.filament.material; if (spool.filament.weight) { const remaining_weight = spool.remaining_weight ?? spool.filament.weight; - str += `${formatWeight(remaining_weight, 0)} / ${formatWeight(spool.filament.weight, 0)}`; + str += ` \u00B7 ${formatWeight(remaining_weight, 0)} / ${formatWeight(spool.filament.weight, 0)}`; } if (spool.last_used) { - // Format like "last used X time ago" const dt = dayjs(spool.last_used); - str += ` - ${t("spool.formats.last_used", { date: dt.fromNow() })}`; + str += ` \u00B7 ${dt.fromNow()}`; } return str; } @@ -164,7 +178,7 @@ export function SpoolCard({ #{spool.id} {filament_name} -
+
+
+
+
); diff --git a/client/src/pages/locations/components/spoolList.tsx b/client/src/pages/locations/components/spoolList.tsx index 46293c407..61deb3944 100644 --- a/client/src/pages/locations/components/spoolList.tsx +++ b/client/src/pages/locations/components/spoolList.tsx @@ -1,9 +1,6 @@ -import { theme } from "antd"; import { ISpool } from "../../spools/model"; import { SpoolCard } from "./spoolCard"; -const { useToken } = theme; - export function SpoolList({ spools, spoolOrder, @@ -13,8 +10,6 @@ export function SpoolList({ spoolOrder: number[]; setSpoolOrder: (spoolOrder: number[]) => void; }) { - const { token } = useToken(); - // Make sure all spools are in the spoolOrders array const finalSpoolOrder = [...spoolOrder].filter((id) => spools.find((spool) => spool.id === id)); // Remove any spools that are not in the spools array spools.forEach((spool) => { @@ -39,13 +34,8 @@ export function SpoolList({ setSpoolOrder(newSpoolOrder); }; - const style = { - backgroundColor: token.colorBgContainer, - borderRadius: token.borderRadiusLG, - }; - return ( -
+
{spools.map((spool, idx) => ( ))} diff --git a/client/src/pages/locations/index.tsx b/client/src/pages/locations/index.tsx index 3b38e0b20..1c7e5f880 100644 --- a/client/src/pages/locations/index.tsx +++ b/client/src/pages/locations/index.tsx @@ -1,4 +1,4 @@ -import { useTranslate } from "@refinedev/core"; +import { List } from "@refinedev/antd"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { DndProvider } from "react-dnd"; @@ -10,14 +10,12 @@ import "./locations.css"; dayjs.extend(utc); export const Locations = () => { - const t = useTranslate(); return ( -
-

{t("locations.locations")}

+ -
+ ); }; diff --git a/client/src/pages/locations/locations.css b/client/src/pages/locations/locations.css index 357d502cf..e5ecc82cc 100644 --- a/client/src/pages/locations/locations.css +++ b/client/src/pages/locations/locations.css @@ -1,11 +1,20 @@ .loc-metacontainer { - display: flex; - flex-wrap: wrap; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(340px, 1fr)); + gap: 16px; + padding: 0; } .loc-container { - padding: 1em; - width: 24em; + border-radius: 12px; + overflow: hidden; + display: flex; + flex-direction: column; + transition: box-shadow 0.2s ease; +} + +.loc-container:hover { + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); } .loc-container.grabable, @@ -16,59 +25,156 @@ cursor: -webkit-grab; } +.loc-header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); +} + .loc-container h3 { display: flex; align-items: center; justify-content: space-between; - font-size: 21px; + font-size: 16px; + font-weight: 600; width: 100%; + margin: 0; + padding: 12px 16px; + border-bottom: 1px solid rgba(255, 255, 255, 0.06); } .loc-container h3 span { cursor: default; + display: flex; + align-items: center; + gap: 8px; } .loc-container h3 span.editable { cursor: text; } +.loc-container h3 span.editable:hover { + opacity: 0.8; +} + .loc-container h3 input { - font-size: 21px; + font-size: 16px; + font-weight: 600; margin: 0; padding: 0; border: 0; + background: transparent; +} + +.loc-spool-count { + font-size: 12px; + font-weight: 500; + padding: 2px 8px; + border-radius: 10px; + white-space: nowrap; } .loc-container .loc-spools { - padding: 0.2em; + padding: 8px; display: flex; flex-direction: column; - gap: 0.5em; - min-height: 3em; - overflow-y: scroll; + gap: 6px; + min-height: 60px; + max-height: 500px; + overflow-y: auto; + flex: 1; +} + +.loc-container .loc-spools::-webkit-scrollbar { + width: 4px; +} + +.loc-container .loc-spools::-webkit-scrollbar-thumb { + border-radius: 4px; + background: rgba(255, 255, 255, 0.1); } .loc-container .spool { - padding: 0.5em 0.5em 0.5em 0; + padding: 10px 12px; display: flex; align-items: center; - border-radius: 0.5em; + border-radius: 8px; + gap: 10px; + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.loc-container .spool:hover { + transform: translateY(-1px); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } .loc-container .spool .info { display: flex; flex-direction: column; width: 100%; + min-width: 0; } .loc-container .spool .info .title { - font-size: 1em; + font-size: 13px; + font-weight: 500; display: flex; align-items: center; justify-content: space-between; width: 100%; + gap: 8px; +} + +.loc-container .spool .info .title > span { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } .loc-container .spool .info .subtitle { - font-size: 0.8em; + font-size: 12px; + margin-top: 2px; + opacity: 0.7; +} + +.loc-container .spool .spool-weight-bar { + width: 100%; + height: 3px; + border-radius: 2px; + margin-top: 6px; + overflow: hidden; +} + +.loc-container .spool .spool-weight-bar .spool-weight-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s ease; +} + +.newLocContainer { + display: flex; + align-items: center; + justify-content: center; + min-height: 120px; + border-radius: 12px; + border: 2px dashed rgba(255, 255, 255, 0.1); + transition: border-color 0.2s ease, background 0.2s ease; +} + +.newLocContainer:hover { + border-color: rgba(255, 255, 255, 0.25); + background: rgba(255, 255, 255, 0.02); +} + +.loc-empty-state { + display: flex; + align-items: center; + justify-content: center; + padding: 24px; + opacity: 0.4; + font-size: 13px; + font-style: italic; } From bac0b8707dc08f4761a8bb20d5bb47d7b647da0a Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:11:46 +0100 Subject: [PATCH 02/19] Fix spool select table on print page to expand like other tables Replace fixed 200px scroll height with horizontal max-content scroll to match the table behavior on other pages. --- client/src/pages/printing/spoolSelectModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/client/src/pages/printing/spoolSelectModal.tsx b/client/src/pages/printing/spoolSelectModal.tsx index 91d8fdb36..32a960ca6 100644 --- a/client/src/pages/printing/spoolSelectModal.tsx +++ b/client/src/pages/printing/spoolSelectModal.tsx @@ -126,7 +126,7 @@ const SpoolSelectModal = ({ description, onContinue }: Props) => { tableLayout="auto" dataSource={dataSource} pagination={false} - scroll={{ y: 200 }} + scroll={{ x: "max-content" }} columns={removeUndefined([ { width: 50, From d0373068c72a32d31c4db7acc3705bd40b4c1d03 Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:12:57 +0100 Subject: [PATCH 03/19] Add modal dialog for creating new locations Replace the auto-naming behavior with a modal that lets the user enter a location name, with validation for empty and duplicate names. --- .../components/locationContainer.tsx | 58 +++++++++++++++---- 1 file changed, 46 insertions(+), 12 deletions(-) diff --git a/client/src/pages/locations/components/locationContainer.tsx b/client/src/pages/locations/components/locationContainer.tsx index 726a875e0..d007ac39a 100644 --- a/client/src/pages/locations/components/locationContainer.tsx +++ b/client/src/pages/locations/components/locationContainer.tsx @@ -1,7 +1,7 @@ import { PlusOutlined } from "@ant-design/icons"; import { useList, useTranslate } from "@refinedev/core"; -import { Button, Spin } from "antd"; -import { useEffect, useMemo } from "react"; +import { Button, Input, Modal, Spin } from "antd"; +import { useEffect, useMemo, useState } from "react"; import { useSetSetting } from "../../../utils/querySettings"; import { ISpool } from "../../spools/model"; import { EMPTYLOC, useLocations, useLocationsSpoolOrders, useRenameSpoolLocation } from "../functions"; @@ -162,19 +162,27 @@ export function LocationContainer() { return
Failed to load spools
; } - const addNewLocation = () => { - const baseLocationName = t("locations.new_location"); - let newLocationName = baseLocationName; + const [modalOpen, setModalOpen] = useState(false); + const [newLocationName, setNewLocationName] = useState(""); + const [modalError, setModalError] = useState(""); - const newLocs = [...locationsList]; - let i = 1; - while (newLocs.includes(newLocationName)) { - newLocationName = baseLocationName + " " + i; - i++; + const addNewLocation = () => { + const name = newLocationName.trim(); + if (!name) { + setModalError(t("locations.error_empty") || "Name cannot be empty"); + return; + } + if (locationsList.includes(name)) { + setModalError(t("locations.error_exists") || "Location already exists"); + return; } - newLocs.push(newLocationName); + const newLocs = [...locationsList]; + newLocs.push(name); setLocationsSetting.mutate(newLocs); + setModalOpen(false); + setNewLocationName(""); + setModalError(""); }; // Count totals @@ -183,12 +191,38 @@ export function LocationContainer() { return (
+ { + setModalOpen(false); + setNewLocationName(""); + setModalError(""); + }} + okText={t("buttons.create") || "Create"} + okButtonProps={{ disabled: !newLocationName.trim() }} + > + { + setNewLocationName(e.target.value); + setModalError(""); + }} + onPressEnter={addNewLocation} + status={modalError ? "error" : undefined} + style={{ marginTop: 8 }} + /> + {modalError &&
{modalError}
} +
{totalSpools} {t("spool.spool", { count: totalSpools })} in {totalLocations}{" "} {t("locations.locations").toLowerCase()} -
From 01e0f774bef0a7d93573d6603ae3fdd41c9a345b Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:33:50 +0100 Subject: [PATCH 04/19] Redesign dashboard with industrial layout and soften dark theme Rebuild the home page with KPI cards, low stock alerts, material breakdown bars, recently used timeline, and location overview in a 2:1 grid layout. Soften the global dark theme backgrounds from pure black to warmer dark grays. Add i18n keys for all new dashboard strings across all 28 locales. --- client/public/locales/cs/common.json | 14 +- client/public/locales/da/common.json | 14 +- client/public/locales/de/common.json | 14 +- client/public/locales/el/common.json | 14 +- client/public/locales/en/common.json | 14 +- client/public/locales/es/common.json | 14 +- client/public/locales/et/common.json | 14 + client/public/locales/fa/common.json | 14 +- client/public/locales/fr/common.json | 14 +- client/public/locales/hi-Latn/common.json | 14 + client/public/locales/hu/common.json | 14 +- client/public/locales/it/common.json | 14 +- client/public/locales/ja/common.json | 14 +- client/public/locales/lt/common.json | 14 +- client/public/locales/nb-NO/common.json | 14 +- client/public/locales/nl/common.json | 14 +- client/public/locales/pl/common.json | 14 +- client/public/locales/pt-BR/common.json | 14 +- client/public/locales/pt/common.json | 14 +- client/public/locales/ro/common.json | 14 +- client/public/locales/ru/common.json | 14 +- client/public/locales/sv/common.json | 14 +- client/public/locales/ta/common.json | 14 +- client/public/locales/th/common.json | 14 +- client/public/locales/tr/common.json | 14 +- client/public/locales/uk/common.json | 14 +- client/public/locales/zh-Hant/common.json | 14 +- client/public/locales/zh/common.json | 14 +- client/src/contexts/color-mode/index.tsx | 8 + client/src/pages/home/home.css | 390 ++++++++++++++++++ client/src/pages/home/index.tsx | 461 +++++++++++++++++----- 31 files changed, 1111 insertions(+), 140 deletions(-) create mode 100644 client/src/pages/home/home.css diff --git a/client/public/locales/cs/common.json b/client/public/locales/cs/common.json index cc4313b5d..84f35b75e 100644 --- a/client/public/locales/cs/common.json +++ b/client/public/locales/cs/common.json @@ -352,7 +352,15 @@ "home": { "home": "Domů", "description": "Vypadá to, že jste ještě nepřidali žádné cívky. Úvodní nápovědu najdete na stránce Help page .", - "welcome": "Vítejte ve vaší instanci Spoolman!" + "welcome": "Vítejte ve vaší instanci Spoolman!", + "total_weight": "Celkový stav", + "total_value": "Hodnota", + "low_stock": "Nízký stav", + "all_stocked": "Všechny cívky jsou dobře zásobeny", + "recently_used": "Naposledy použité", + "no_recent": "Žádné nedávno použité cívky", + "by_material": "Podle materiálu", + "by_location": "Podle umístění" }, "settings": { "extra_fields": { @@ -407,6 +415,8 @@ "locations": "Umístění", "no_locations_help": "Na této stránce můžete uspořádat své cívky podle umístění, přidejte několik cívek a začněte!", "new_location": "Nové umístění", - "no_location": "Žádné umístění" + "no_location": "Žádné umístění", + "error_empty": "Název nesmí být prázdný", + "error_exists": "Umístění již existuje" } } diff --git a/client/public/locales/da/common.json b/client/public/locales/da/common.json index 42919410e..7fabc931a 100644 --- a/client/public/locales/da/common.json +++ b/client/public/locales/da/common.json @@ -316,7 +316,15 @@ }, "kofi": "Donér på Ko-fi", "home": { - "home": "Hjem" + "home": "Hjem", + "total_weight": "Samlet beholdning", + "total_value": "Værdi", + "low_stock": "Lav beholdning", + "all_stocked": "Alle spoler er godt fyldt op", + "recently_used": "Senest brugt", + "no_recent": "Ingen nyligt brugte spoler", + "by_material": "Efter materiale", + "by_location": "Efter placering" }, "settings": { "header": "Indstillinger", @@ -358,5 +366,9 @@ "delete_confirm_description": "Dette vil slette feltet samt alle associerede data for alle poster." }, "settings": "Indstillinger" + }, + "locations": { + "error_empty": "Navn må ikke være tomt", + "error_exists": "Placering findes allerede" } } diff --git a/client/public/locales/de/common.json b/client/public/locales/de/common.json index b39d18fa7..d04eb1df2 100644 --- a/client/public/locales/de/common.json +++ b/client/public/locales/de/common.json @@ -351,7 +351,15 @@ "home": { "home": "Home", "welcome": "Willkommen auf deiner Spoolman Instanz!", - "description": "Es sieht so aus, als hättest du noch keine Spulen hinzugefügt. Schau auf unsere Hilfeseite, falls du Hilfe beim Start benötigst." + "description": "Es sieht so aus, als hättest du noch keine Spulen hinzugefügt. Schau auf unsere Hilfeseite, falls du Hilfe beim Start benötigst.", + "total_weight": "Gesamtbestand", + "total_value": "Wert", + "low_stock": "Niedriger Bestand", + "all_stocked": "Alle Spulen sind gut bevorratet", + "recently_used": "Zuletzt verwendet", + "no_recent": "Keine kürzlich verwendeten Spulen", + "by_material": "Nach Material", + "by_location": "Nach Standort" }, "settings": { "header": "Einstellungen", @@ -406,6 +414,8 @@ "no_location": "Kein Ort", "no_locations_help": "Diese Seite lässt Sie Ihre Spulen zu Orten zuweisen, fügen Sie ein paar Spulen hinzu um loszulegen!", "locations": "Orte", - "new_location": "Neuer Ort" + "new_location": "Neuer Ort", + "error_empty": "Name darf nicht leer sein", + "error_exists": "Ort existiert bereits" } } diff --git a/client/public/locales/el/common.json b/client/public/locales/el/common.json index 490b9a33b..faf308196 100644 --- a/client/public/locales/el/common.json +++ b/client/public/locales/el/common.json @@ -288,7 +288,15 @@ }, "kofi": "Φιλοδώρημα στο Ko-fi", "home": { - "home": "Αρχική" + "home": "Αρχική", + "total_weight": "Συνολικό απόθεμα", + "total_value": "Αξία", + "low_stock": "Χαμηλό απόθεμα", + "all_stocked": "Όλες οι κουβαρίστρες είναι καλά εφοδιασμένες", + "recently_used": "Πρόσφατα χρησιμοποιημένα", + "no_recent": "Δεν υπάρχουν πρόσφατα χρησιμοποιημένες κουβαρίστρες", + "by_material": "Ανά υλικό", + "by_location": "Ανά τοποθεσία" }, "settings": { "extra_fields": { @@ -330,5 +338,9 @@ }, "header": "Ρυθμίσεις", "settings": "Ρυθμίσεις" + }, + "locations": { + "error_empty": "Το όνομα δεν μπορεί να είναι κενό", + "error_exists": "Η τοποθεσία υπάρχει ήδη" } } diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index 88ff2ae85..ec9255c81 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -293,7 +293,15 @@ "home": { "home": "Home", "welcome": "Welcome to your Spoolman instance!", - "description": "It looks like you haven't added any spools yet. See the Help page for help getting started." + "description": "It looks like you haven't added any spools yet. See the Help page for help getting started.", + "total_weight": "Total Stock", + "total_value": "Value", + "low_stock": "Low Stock", + "all_stocked": "All spools are well stocked", + "recently_used": "Recently Used", + "no_recent": "No recently used spools", + "by_material": "By Material", + "by_location": "By Location" }, "help": { "help": "Help", @@ -394,6 +402,8 @@ "locations": "Locations", "new_location": "New Location", "no_location": "No Location", - "no_locations_help": "This page lets you organize your spools in locations, add some spools to get started!" + "no_locations_help": "This page lets you organize your spools in locations, add some spools to get started!", + "error_empty": "Name cannot be empty", + "error_exists": "Location already exists" } } diff --git a/client/public/locales/es/common.json b/client/public/locales/es/common.json index 18e22b98d..9820c9493 100644 --- a/client/public/locales/es/common.json +++ b/client/public/locales/es/common.json @@ -352,7 +352,15 @@ "home": { "home": "Inicio", "welcome": "¡Bienvenido a tu instancia de Spoolman!", - "description": "Parece que aún no has añadido ninguna bobina aún. Visita la página de ayuda para comenzar." + "description": "Parece que aún no has añadido ninguna bobina aún. Visita la página de ayuda para comenzar.", + "total_weight": "Stock total", + "total_value": "Valor", + "low_stock": "Stock bajo", + "all_stocked": "Todas las bobinas están bien abastecidas", + "recently_used": "Usadas recientemente", + "no_recent": "No hay bobinas usadas recientemente", + "by_material": "Por material", + "by_location": "Por ubicación" }, "settings": { "header": "Ajustes", @@ -407,6 +415,8 @@ "locations": "Ubicaciones", "new_location": "Nueva ubicación", "no_location": "Sin ubicación", - "no_locations_help": "Esta página te permite organizar tus carretes en ubicaciones, ¡añade algunos carretes para empezar!" + "no_locations_help": "Esta página te permite organizar tus carretes en ubicaciones, ¡añade algunos carretes para empezar!", + "error_empty": "El nombre no puede estar vacío", + "error_exists": "La ubicación ya existe" } } diff --git a/client/public/locales/et/common.json b/client/public/locales/et/common.json index ececeb30c..c8d05bb53 100644 --- a/client/public/locales/et/common.json +++ b/client/public/locales/et/common.json @@ -62,5 +62,19 @@ "generic": { "title": "Trükkib" } + }, + "home": { + "total_weight": "Koguvaru", + "total_value": "Väärtus", + "low_stock": "Madal varu", + "all_stocked": "Kõik poolid on hästi varustatud", + "recently_used": "Hiljuti kasutatud", + "no_recent": "Hiljuti kasutatud poole pole", + "by_material": "Materjali järgi", + "by_location": "Asukoha järgi" + }, + "locations": { + "error_empty": "Nimi ei tohi olla tühi", + "error_exists": "Asukoht on juba olemas" } } diff --git a/client/public/locales/fa/common.json b/client/public/locales/fa/common.json index e695407f1..bc116d70b 100644 --- a/client/public/locales/fa/common.json +++ b/client/public/locales/fa/common.json @@ -288,7 +288,15 @@ } }, "home": { - "home": "صفحه اصلی" + "home": "صفحه اصلی", + "total_weight": "موجودی کل", + "total_value": "ارزش", + "low_stock": "موجودی کم", + "all_stocked": "همه قرقره‌ها به خوبی ذخیره شده‌اند", + "recently_used": "اخیراً استفاده شده", + "no_recent": "قرقره‌ای اخیراً استفاده نشده", + "by_material": "بر اساس مواد", + "by_location": "بر اساس مکان" }, "help": { "help": "راهنما", @@ -377,5 +385,9 @@ "create": "تعریف کردن تولید کننده فیلامنت جدید | اسپول من", "clone": "تکثیر تولید کننده فیلامنت شماره {{id}} | اسپول من" } + }, + "locations": { + "error_empty": "نام نمی‌تواند خالی باشد", + "error_exists": "مکان از قبل وجود دارد" } } diff --git a/client/public/locales/fr/common.json b/client/public/locales/fr/common.json index becc4697f..95199ee2a 100644 --- a/client/public/locales/fr/common.json +++ b/client/public/locales/fr/common.json @@ -351,7 +351,15 @@ "kofi": "Me donner un pourboire sur Ko-fi", "home": { "home": "Accueil", - "welcome": "Bienvenue sur votre instance Spoolman !" + "welcome": "Bienvenue sur votre instance Spoolman !", + "total_weight": "Stock total", + "total_value": "Valeur", + "low_stock": "Stock faible", + "all_stocked": "Toutes les bobines sont bien approvisionnées", + "recently_used": "Utilisées récemment", + "no_recent": "Aucune bobine utilisée récemment", + "by_material": "Par matériau", + "by_location": "Par emplacement" }, "settings": { "settings": "Paramètres", @@ -402,6 +410,8 @@ "locations": "Emplacements", "new_location": "Créer un emplacement", "no_location": "Pas de localisation", - "no_locations_help": "Cette page vous permet d'organiser vos bobines par lieu, ajoutez une bobine pour commencer !" + "no_locations_help": "Cette page vous permet d'organiser vos bobines par lieu, ajoutez une bobine pour commencer !", + "error_empty": "Le nom ne peut pas être vide", + "error_exists": "L'emplacement existe déjà" } } diff --git a/client/public/locales/hi-Latn/common.json b/client/public/locales/hi-Latn/common.json index 88011dd4a..722d0226f 100644 --- a/client/public/locales/hi-Latn/common.json +++ b/client/public/locales/hi-Latn/common.json @@ -13,5 +13,19 @@ "confirm": "Kya aapko pakka hai?", "continue": "Aage badhen", "show": "Dikhayen" + }, + "home": { + "total_weight": "Kul stock", + "total_value": "Mulya", + "low_stock": "Kam stock", + "all_stocked": "Sabhi spool achhe se stock hain", + "recently_used": "Haal hi mein istemal ki gayi", + "no_recent": "Koi haal hi mein istemal ki gayi spool nahi", + "by_material": "Material ke anusaar", + "by_location": "Jagah ke anusaar" + }, + "locations": { + "error_empty": "Naam khaali nahi ho sakta", + "error_exists": "Jagah pehle se maujood hai" } } diff --git a/client/public/locales/hu/common.json b/client/public/locales/hu/common.json index 7e0e7e275..6df77e01f 100644 --- a/client/public/locales/hu/common.json +++ b/client/public/locales/hu/common.json @@ -277,7 +277,15 @@ }, "kofi": "Tipp a Ko-fi-ra", "home": { - "home": "Kezdő lap" + "home": "Kezdő lap", + "total_weight": "Teljes készlet", + "total_value": "Érték", + "low_stock": "Alacsony készlet", + "all_stocked": "Minden orsó jól feltöltve", + "recently_used": "Nemrég használt", + "no_recent": "Nincsenek nemrég használt orsók", + "by_material": "Anyag szerint", + "by_location": "Hely szerint" }, "help": { "resources": { @@ -328,5 +336,9 @@ }, "tab": "Általános" } + }, + "locations": { + "error_empty": "A név nem lehet üres", + "error_exists": "A hely már létezik" } } diff --git a/client/public/locales/it/common.json b/client/public/locales/it/common.json index e45aec76a..82ad269d1 100644 --- a/client/public/locales/it/common.json +++ b/client/public/locales/it/common.json @@ -352,7 +352,15 @@ "home": { "home": "Home", "welcome": "Benvenuto nella tua istanza Spoolman!", - "description": "Sembra che tu non abbia ancora aggiunto alcuna bobina. Vai su Help page per ricevere assistenza su come iniziare." + "description": "Sembra che tu non abbia ancora aggiunto alcuna bobina. Vai su Help page per ricevere assistenza su come iniziare.", + "total_weight": "Stock totale", + "total_value": "Valore", + "low_stock": "Scorte basse", + "all_stocked": "Tutte le bobine sono ben rifornite", + "recently_used": "Usate di recente", + "no_recent": "Nessuna bobina usata di recente", + "by_material": "Per materiale", + "by_location": "Per posizione" }, "settings": { "settings": "Configurazione", @@ -407,6 +415,8 @@ "locations": "Posizioni", "new_location": "Nuova Posizione", "no_location": "Nessuna Posizione", - "no_locations_help": "Questa pagina ti consente di organizzare le bobine in base alla posizione. Aggiungi alcune bobine per iniziare!" + "no_locations_help": "Questa pagina ti consente di organizzare le bobine in base alla posizione. Aggiungi alcune bobine per iniziare!", + "error_empty": "Il nome non può essere vuoto", + "error_exists": "La posizione esiste già" } } diff --git a/client/public/locales/ja/common.json b/client/public/locales/ja/common.json index 951fc1b11..6c05eafe6 100644 --- a/client/public/locales/ja/common.json +++ b/client/public/locales/ja/common.json @@ -388,9 +388,19 @@ "locations": "場所", "new_location": "新しい場所", "no_location": "場所なし", - "no_locations_help": "このページでは、スプールを場所ごとに整理することができます!" + "no_locations_help": "このページでは、スプールを場所ごとに整理することができます!", + "error_empty": "名前を入力してください", + "error_exists": "この場所は既に存在します" }, "home": { - "home": "ホーム" + "home": "ホーム", + "total_weight": "総在庫", + "total_value": "総額", + "low_stock": "在庫不足", + "all_stocked": "すべてのスプールは十分な在庫があります", + "recently_used": "最近使用", + "no_recent": "最近使用したスプールはありません", + "by_material": "素材別", + "by_location": "場所別" } } diff --git a/client/public/locales/lt/common.json b/client/public/locales/lt/common.json index 2dbd9ca79..c9ac66f43 100644 --- a/client/public/locales/lt/common.json +++ b/client/public/locales/lt/common.json @@ -294,7 +294,15 @@ "home": { "home": "Pradžia", "welcome": "Sveiki atvykę į savo Spoolman!", - "description": "Panašu, kad dar nepridėjote jokių ričių. Norėdami gauti pagalbos, kaip pradėti, žr. pagalbos puslapį." + "description": "Panašu, kad dar nepridėjote jokių ričių. Norėdami gauti pagalbos, kaip pradėti, žr. pagalbos puslapį.", + "total_weight": "Bendras kiekis", + "total_value": "Vertė", + "low_stock": "Mažas kiekis", + "all_stocked": "Visos ritės gerai aprūpintos", + "recently_used": "Neseniai naudotos", + "no_recent": "Nėra neseniai naudotų ričių", + "by_material": "Pagal medžiagą", + "by_location": "Pagal vietą" }, "help": { "help": "Pagalba", @@ -395,6 +403,8 @@ "locations": "Vietos", "new_location": "Nauja vieta", "no_location": "Be vietos", - "no_locations_help": "Šiame puslapyje galite tvarkyti rites pagal vietas, pridėkite keletą ričių, kad pradėtumėte!" + "no_locations_help": "Šiame puslapyje galite tvarkyti rites pagal vietas, pridėkite keletą ričių, kad pradėtumėte!", + "error_empty": "Pavadinimas negali būti tuščias", + "error_exists": "Vieta jau egzistuoja" } } diff --git a/client/public/locales/nb-NO/common.json b/client/public/locales/nb-NO/common.json index f98639776..117c4d74e 100644 --- a/client/public/locales/nb-NO/common.json +++ b/client/public/locales/nb-NO/common.json @@ -342,7 +342,15 @@ "home": { "home": "Hjem", "welcome": "Velkommen til Spoolman!", - "description": "Det ser ut som du ikke har lagt til noen spoler ennå. Se hjelpesiden for hjelp til å komme i gang." + "description": "Det ser ut som du ikke har lagt til noen spoler ennå. Se hjelpesiden for hjelp til å komme i gang.", + "total_weight": "Totalt lager", + "total_value": "Verdi", + "low_stock": "Lavt lager", + "all_stocked": "Alle spoler er godt fylt opp", + "recently_used": "Nylig brukt", + "no_recent": "Ingen nylig brukte spoler", + "by_material": "Etter materiale", + "by_location": "Etter plassering" }, "help": { "help": "Hjelp", @@ -357,7 +365,9 @@ "locations": "Lokasjoner", "new_location": "Ny lokasjon", "no_location": "Ingen lokasjon", - "no_locations_help": "Denne siden mar deg organisere rullene med filament i lokasjoner. Legg til noen ruller for å starte!" + "no_locations_help": "Denne siden mar deg organisere rullene med filament i lokasjoner. Legg til noen ruller for å starte!", + "error_empty": "Navn kan ikke være tomt", + "error_exists": "Plasseringen finnes allerede" }, "settings": { "settings": "Innstillinger", diff --git a/client/public/locales/nl/common.json b/client/public/locales/nl/common.json index caac3566c..a14853b4e 100644 --- a/client/public/locales/nl/common.json +++ b/client/public/locales/nl/common.json @@ -349,7 +349,15 @@ }, "kofi": "Tip mij op Ko-fi", "home": { - "home": "Thuis" + "home": "Thuis", + "total_weight": "Totale voorraad", + "total_value": "Waarde", + "low_stock": "Lage voorraad", + "all_stocked": "Alle spoelen zijn goed bevoorraad", + "recently_used": "Recent gebruikt", + "no_recent": "Geen recent gebruikte spoelen", + "by_material": "Per materiaal", + "by_location": "Per locatie" }, "settings": { "header": "Instellingen", @@ -404,6 +412,8 @@ "no_location": "Geen locatie", "no_locations_help": "Op deze pagina kunt u uw spoelen op verschillende locaties ordenen. Voeg spoelen toe om aan de slag te gaan!", "locations": "Locaties", - "new_location": "Nieuwe locatie" + "new_location": "Nieuwe locatie", + "error_empty": "Naam mag niet leeg zijn", + "error_exists": "Locatie bestaat al" } } diff --git a/client/public/locales/pl/common.json b/client/public/locales/pl/common.json index 2bb533c0d..4eda705d1 100644 --- a/client/public/locales/pl/common.json +++ b/client/public/locales/pl/common.json @@ -352,7 +352,15 @@ "home": { "home": "Strona główna", "welcome": "Witaj w instancji Spoolman!", - "description": "Wygląda na to, że nie dodano jeszcze żadnych szpul. Zobacz stronę pomocy, aby dowiedzieć się jak rozpocząć." + "description": "Wygląda na to, że nie dodano jeszcze żadnych szpul. Zobacz stronę pomocy, aby dowiedzieć się jak rozpocząć.", + "total_weight": "Łączny zapas", + "total_value": "Wartość", + "low_stock": "Niski zapas", + "all_stocked": "Wszystkie szpule są dobrze zaopatrzone", + "recently_used": "Ostatnio używane", + "no_recent": "Brak ostatnio używanych szpul", + "by_material": "Wg materiału", + "by_location": "Wg lokalizacji" }, "settings": { "settings": "Ustawienia", @@ -407,6 +415,8 @@ "locations": "Lokalizacje", "new_location": "Nowa lokalizacja", "no_location": "Brak lokalizacji", - "no_locations_help": "Ta strona pozwala uporządkować szpule w lokalizacjach, dodaj kilka szpul aby rozpocząć!" + "no_locations_help": "Ta strona pozwala uporządkować szpule w lokalizacjach, dodaj kilka szpul aby rozpocząć!", + "error_empty": "Nazwa nie może być pusta", + "error_exists": "Lokalizacja już istnieje" } } diff --git a/client/public/locales/pt-BR/common.json b/client/public/locales/pt-BR/common.json index 349c6fe37..814a6be37 100644 --- a/client/public/locales/pt-BR/common.json +++ b/client/public/locales/pt-BR/common.json @@ -294,7 +294,15 @@ "home": { "home": "Início", "welcome": "Bem-vindo à sua instância do Spoolman!", - "description": "Parece que você ainda não adicionou nenhum carretel. Veja a Página de Ajuda para obter ajuda sobre como começar." + "description": "Parece que você ainda não adicionou nenhum carretel. Veja a Página de Ajuda para obter ajuda sobre como começar.", + "total_weight": "Estoque total", + "total_value": "Valor", + "low_stock": "Estoque baixo", + "all_stocked": "Todas as bobinas estão bem abastecidas", + "recently_used": "Usadas recentemente", + "no_recent": "Nenhuma bobina usada recentemente", + "by_material": "Por material", + "by_location": "Por localização" }, "help": { "help": "Ajuda", @@ -395,6 +403,8 @@ "locations": "Locais", "new_location": "Novo Local", "no_location": "Sem Local", - "no_locations_help": "Esta página permite que você organize seus carretéis em locais, adicione alguns carretéis para começar!" + "no_locations_help": "Esta página permite que você organize seus carretéis em locais, adicione alguns carretéis para começar!", + "error_empty": "O nome não pode estar vazio", + "error_exists": "A localização já existe" } } diff --git a/client/public/locales/pt/common.json b/client/public/locales/pt/common.json index 9eb6fada7..01728fdce 100644 --- a/client/public/locales/pt/common.json +++ b/client/public/locales/pt/common.json @@ -293,7 +293,15 @@ "home": { "home": "Início", "welcome": "Bem vindo à sua instância do Spoolman!", - "description": "Parece que ainda não adicionou nenhuma bobine. Veja a Página de Ajuda para obter ajuda a começar." + "description": "Parece que ainda não adicionou nenhuma bobine. Veja a Página de Ajuda para obter ajuda a começar.", + "total_weight": "Stock total", + "total_value": "Valor", + "low_stock": "Stock baixo", + "all_stocked": "Todas as bobinas estão bem abastecidas", + "recently_used": "Usadas recentemente", + "no_recent": "Sem bobinas usadas recentemente", + "by_material": "Por material", + "by_location": "Por localização" }, "help": { "help": "Ajuda", @@ -407,6 +415,8 @@ "locations": "Locais", "new_location": "Novo Local", "no_location": "Sem Local", - "no_locations_help": "Esta página permite organizar as suas bobines em locais, adicione algumas bobines para começar!" + "no_locations_help": "Esta página permite organizar as suas bobines em locais, adicione algumas bobines para começar!", + "error_empty": "O nome não pode estar vazio", + "error_exists": "A localização já existe" } } diff --git a/client/public/locales/ro/common.json b/client/public/locales/ro/common.json index a87852591..82171b76a 100644 --- a/client/public/locales/ro/common.json +++ b/client/public/locales/ro/common.json @@ -337,7 +337,15 @@ "home": { "home": "Acasă", "welcome": "Bine ai venit în instanța ta Spoolman!", - "description": "Se pare că nu ați adăugat încă nici o rolă. Consultați pagina Ajutor pentru a începe." + "description": "Se pare că nu ați adăugat încă nici o rolă. Consultați pagina Ajutor pentru a începe.", + "total_weight": "Stoc total", + "total_value": "Valoare", + "low_stock": "Stoc scăzut", + "all_stocked": "Toate bobinele sunt bine aprovizionate", + "recently_used": "Utilizate recent", + "no_recent": "Nicio bobină utilizată recent", + "by_material": "După material", + "by_location": "După locație" }, "scanner": { "error": { @@ -407,6 +415,8 @@ "locations": "Zone", "new_location": "Zonă nouă", "no_location": "Fără zonă", - "no_locations_help": "Această pagină îți permite să îți organizezi rolele în zone, adaugă câteva role pentru a începe!" + "no_locations_help": "Această pagină îți permite să îți organizezi rolele în zone, adaugă câteva role pentru a începe!", + "error_empty": "Numele nu poate fi gol", + "error_exists": "Locația există deja" } } diff --git a/client/public/locales/ru/common.json b/client/public/locales/ru/common.json index b73ee07fe..4f744b5b4 100644 --- a/client/public/locales/ru/common.json +++ b/client/public/locales/ru/common.json @@ -352,7 +352,15 @@ "home": { "home": "Главная", "welcome": "Добро пожаловать в ваш личный Spoolman!", - "description": "Похоже, что вы ещё не добавили ни одной катушки. Посетите Страницу помощи, для комфортного начала." + "description": "Похоже, что вы ещё не добавили ни одной катушки. Посетите Страницу помощи, для комфортного начала.", + "total_weight": "Общий запас", + "total_value": "Стоимость", + "low_stock": "Мало на складе", + "all_stocked": "Все катушки хорошо укомплектованы", + "recently_used": "Недавно использованные", + "no_recent": "Нет недавно использованных катушек", + "by_material": "По материалу", + "by_location": "По расположению" }, "settings": { "extra_fields": { @@ -407,6 +415,8 @@ "no_locations_help": "Эта страница позволяет организовать катушки по местоположениям. Добавьте несколько катушек, чтобы начать!", "locations": "Местоположения", "new_location": "Новое местоположение", - "no_location": "Нет местоположения" + "no_location": "Нет местоположения", + "error_empty": "Название не может быть пустым", + "error_exists": "Расположение уже существует" } } diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index 39977fc0e..16f693324 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -293,7 +293,15 @@ } }, "home": { - "home": "Hem" + "home": "Hem", + "total_weight": "Totalt lager", + "total_value": "Värde", + "low_stock": "Lågt lager", + "all_stocked": "Alla spolar är välfyllda", + "recently_used": "Nyligen använda", + "no_recent": "Inga nyligen använda spolar", + "by_material": "Per material", + "by_location": "Per plats" }, "table": { "actions": "Åtgärder" @@ -382,6 +390,8 @@ "kofi": "Dricksa mig på Ko-fi", "locations": { "new_location": "Ny plats", - "no_location": "Ingen plats" + "no_location": "Ingen plats", + "error_empty": "Namnet får inte vara tomt", + "error_exists": "Platsen finns redan" } } diff --git a/client/public/locales/ta/common.json b/client/public/locales/ta/common.json index 4e30a0ebc..0ea2a6cf3 100644 --- a/client/public/locales/ta/common.json +++ b/client/public/locales/ta/common.json @@ -291,7 +291,9 @@ "locations": "இருப்பிடங்கள்", "new_location": "புதிய இடம்", "no_location": "இடம் இல்லை", - "no_locations_help": "இந்த பக்கம் உங்கள் ச்பூல்களை இருப்பிடங்களில் ஒழுங்கமைக்க அனுமதிக்கிறது, தொடங்குவதற்கு சில ச்பூல்களைச் சேர்க்கவும்!" + "no_locations_help": "இந்த பக்கம் உங்கள் ச்பூல்களை இருப்பிடங்களில் ஒழுங்கமைக்க அனுமதிக்கிறது, தொடங்குவதற்கு சில ச்பூல்களைச் சேர்க்கவும்!", + "error_empty": "பெயர் காலியாக இருக்க முடியாது", + "error_exists": "இடம் ஏற்கனவே உள்ளது" }, "actions": { "list": "பட்டியல்", @@ -342,7 +344,15 @@ "home": { "home": "வீடு", "welcome": "உங்கள் ச்பூல்மேன் உதாரணத்திற்கு வருக!", - "description": "நீங்கள் இதுவரை எந்த ச்பூல்களையும் சேர்க்கவில்லை என்று தெரிகிறது. தொடங்குவதற்கு உதவி பக்கம் ஐப் பார்க்கவும்." + "description": "நீங்கள் இதுவரை எந்த ச்பூல்களையும் சேர்க்கவில்லை என்று தெரிகிறது. தொடங்குவதற்கு உதவி பக்கம் ஐப் பார்க்கவும்.", + "total_weight": "மொத்த இருப்பு", + "total_value": "மதிப்பு", + "low_stock": "குறைந்த இருப்பு", + "all_stocked": "அனைத்து ஸ்பூல்களும் நன்கு நிரப்பப்பட்டுள்ளன", + "recently_used": "சமீபத்தில் பயன்படுத்தியவை", + "no_recent": "சமீபத்தில் பயன்படுத்திய ஸ்பூல்கள் இல்லை", + "by_material": "பொருள் வாரியாக", + "by_location": "இடம் வாரியாக" }, "table": { "actions": "செயல்கள்" diff --git a/client/public/locales/th/common.json b/client/public/locales/th/common.json index 92e08eff8..b85e01fda 100644 --- a/client/public/locales/th/common.json +++ b/client/public/locales/th/common.json @@ -380,10 +380,20 @@ "new_location": "สถานที่เก็บใหม่", "no_location": "ไม่มีสถานที่เก็บ", "locations": "สถานที่เก็บ", - "no_locations_help": "หน้านี้ให้คุณจัดการม้วนพลาสติกตามสถานที่เก็บ เพิ่มม้วนพลาสติกเพื่อเริ่มต้น!" + "no_locations_help": "หน้านี้ให้คุณจัดการม้วนพลาสติกตามสถานที่เก็บ เพิ่มม้วนพลาสติกเพื่อเริ่มต้น!", + "error_empty": "ชื่อต้องไม่ว่างเปล่า", + "error_exists": "ตำแหน่งนี้มีอยู่แล้ว" }, "home": { - "home": "หน้าแรก" + "home": "หน้าแรก", + "total_weight": "สต็อกทั้งหมด", + "total_value": "มูลค่า", + "low_stock": "สต็อกต่ำ", + "all_stocked": "ม้วนทั้งหมดมีสต็อกเพียงพอ", + "recently_used": "ใช้ล่าสุด", + "no_recent": "ไม่มีม้วนที่ใช้ล่าสุด", + "by_material": "ตามวัสดุ", + "by_location": "ตามตำแหน่ง" }, "warnWhenUnsavedChanges": "คุณแน่ใจหรือไม่ว่าต้องการออก? คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก", "table": { diff --git a/client/public/locales/tr/common.json b/client/public/locales/tr/common.json index 8724a9645..a1a8dfeb1 100644 --- a/client/public/locales/tr/common.json +++ b/client/public/locales/tr/common.json @@ -293,7 +293,15 @@ "home": { "home": "Ana Sayfa", "welcome": "Spoolman sunucunuza hoşgeldiniz!", - "description": "Görünüşe göre henüz hiç makara eklememişsiniz. Başlamak için yardım almak isterseniz Yardım sayfasına göz atabilirsiniz." + "description": "Görünüşe göre henüz hiç makara eklememişsiniz. Başlamak için yardım almak isterseniz Yardım sayfasına göz atabilirsiniz.", + "total_weight": "Toplam stok", + "total_value": "Değer", + "low_stock": "Düşük stok", + "all_stocked": "Tüm makaralar iyi stoklanmış", + "recently_used": "Son kullanılan", + "no_recent": "Son kullanılan makara yok", + "by_material": "Malzemeye göre", + "by_location": "Konuma göre" }, "help": { "help": "Yardım", @@ -394,6 +402,8 @@ "locations": "Konumlar", "new_location": "Yeni Konum", "no_location": "Konum Yok", - "no_locations_help": "Bu sayfa, makaralarınızı konumlara göre düzenlemenizi sağlar. Başlamak için birkaç makara ekleyin!" + "no_locations_help": "Bu sayfa, makaralarınızı konumlara göre düzenlemenizi sağlar. Başlamak için birkaç makara ekleyin!", + "error_empty": "Ad boş olamaz", + "error_exists": "Konum zaten mevcut" } } diff --git a/client/public/locales/uk/common.json b/client/public/locales/uk/common.json index 66eff3cbe..fc44b6664 100644 --- a/client/public/locales/uk/common.json +++ b/client/public/locales/uk/common.json @@ -333,7 +333,15 @@ }, "kofi": "Задонатити на Ko-fi", "home": { - "home": "Домашня" + "home": "Домашня", + "total_weight": "Загальний запас", + "total_value": "Вартість", + "low_stock": "Низький запас", + "all_stocked": "Всі котушки добре укомплектовані", + "recently_used": "Нещодавно використані", + "no_recent": "Немає нещодавно використаних котушок", + "by_material": "За матеріалом", + "by_location": "За розташуванням" }, "settings": { "header": "Налаштування", @@ -378,5 +386,9 @@ "description": "

Тут ви можете додати додаткові користувацькі поля до своїх об’єктів.

Після додавання поля ви не можете змінити його ключ або тип, а для полів типу вибору ви не можете видалити варіанти або змінити стан кількох варіантів. Якщо ви вилучите поле, пов’язані дані для всіх об’єктів буде видалено.

Ключ – це те, як інші програми читають/записують дані, тож якщо ваше спеціальне поле має інтегруватися зі сторонніми програми, переконайтеся, що ви правильно її налаштували. Значення за замовчуванням застосовується лише до нових елементів.

Додаткові поля не можна сортувати чи фільтрувати в режимі перегляду таблиці.

" }, "settings": "Налаштування" + }, + "locations": { + "error_empty": "Назва не може бути порожньою", + "error_exists": "Розташування вже існує" } } diff --git a/client/public/locales/zh-Hant/common.json b/client/public/locales/zh-Hant/common.json index 58572be9f..331b90a44 100644 --- a/client/public/locales/zh-Hant/common.json +++ b/client/public/locales/zh-Hant/common.json @@ -324,7 +324,15 @@ } }, "home": { - "home": "主頁" + "home": "主頁", + "total_weight": "總庫存", + "total_value": "總價值", + "low_stock": "庫存不足", + "all_stocked": "所有線軸庫存充足", + "recently_used": "最近使用", + "no_recent": "沒有最近使用的線軸", + "by_material": "按材料", + "by_location": "按位置" }, "help": { "help": "說明", @@ -387,6 +395,8 @@ "locations": "位置", "no_location": "未設定位置", "no_locations_help": "這個頁面讓您將料盤按位置進行整理,請從增加一些線盤來開始!", - "new_location": "新增位置" + "new_location": "新增位置", + "error_empty": "名稱不能為空", + "error_exists": "位置已存在" } } diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index d08b0aac3..6bf13fb5d 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -339,7 +339,15 @@ }, "kofi": "在Ko-fi上给我打赏一点小费", "home": { - "home": "主页" + "home": "主页", + "total_weight": "总库存", + "total_value": "总价值", + "low_stock": "库存不足", + "all_stocked": "所有线轴库存充足", + "recently_used": "最近使用", + "no_recent": "没有最近使用的线轴", + "by_material": "按材料", + "by_location": "按位置" }, "help": { "help": "帮助", @@ -403,6 +411,8 @@ "no_locations_help": "此页面允许您在不同位置管理料盘,添加一些料盘即可开始!", "locations": "位置", "new_location": "新位置", - "no_location": "没有位置" + "no_location": "没有位置", + "error_empty": "名称不能为空", + "error_exists": "位置已存在" } } diff --git a/client/src/contexts/color-mode/index.tsx b/client/src/contexts/color-mode/index.tsx index ee606ce92..8b5db7f26 100644 --- a/client/src/contexts/color-mode/index.tsx +++ b/client/src/contexts/color-mode/index.tsx @@ -42,6 +42,14 @@ export const ColorModeContextProvider = ({ children }: PropsWithChildren) => { algorithm: mode === "light" ? defaultAlgorithm : darkAlgorithm, token: { colorPrimary: "#dc7734", + ...(mode === "dark" + ? { + colorBgBase: "#181818", + colorBgContainer: "#1f1f1f", + colorBgElevated: "#252525", + colorBgLayout: "#141414", + } + : {}), }, }} > diff --git a/client/src/pages/home/home.css b/client/src/pages/home/home.css new file mode 100644 index 000000000..5fa3ef038 --- /dev/null +++ b/client/src/pages/home/home.css @@ -0,0 +1,390 @@ +.dashboard { + max-width: 1400px; + margin: 0 auto; + padding: 0 4px; +} + +/* Header */ +.dashboard-header { + display: flex; + justify-content: space-between; + align-items: flex-end; + margin-bottom: 32px; +} + +.dashboard-header h2 { + font-size: 26px; + font-weight: 800; + letter-spacing: -0.03em; + margin: 0; +} + +.dashboard-header .dash-subtitle { + font-size: 13px; + opacity: 0.4; + margin: 4px 0 0 0; +} + +.dashboard-header .dash-new-btn { + border: none; + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.1em; + padding: 10px 24px; + border-radius: 6px; + cursor: pointer; + display: flex; + align-items: center; + gap: 6px; + transition: opacity 0.2s, transform 0.1s; +} + +.dashboard-header .dash-new-btn:hover { + opacity: 0.9; +} + +.dashboard-header .dash-new-btn:active { + transform: scale(0.97); +} + +/* KPI Grid */ +.kpi-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 16px; + margin-bottom: 32px; +} + +@media (max-width: 1100px) { + .kpi-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 600px) { + .kpi-grid { + grid-template-columns: 1fr; + } +} + +.kpi-card { + padding: 20px 24px; + border-radius: 8px; + position: relative; + overflow: hidden; + border-top: 1px solid rgba(255, 255, 255, 0.03); + transition: transform 0.2s ease, box-shadow 0.2s ease; +} + +.kpi-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.kpi-card .kpi-bg-icon { + position: absolute; + right: -4px; + bottom: -10px; + font-size: 80px !important; + opacity: 0.035; + pointer-events: none; + transition: opacity 0.3s ease; +} + +.kpi-card:hover .kpi-bg-icon { + opacity: 0.07; +} + +.kpi-card .kpi-label { + font-size: 10px; + text-transform: uppercase; + font-weight: 700; + letter-spacing: 0.12em; + opacity: 0.45; + margin-bottom: 6px; +} + +.kpi-card .kpi-value { + font-size: 32px; + font-weight: 800; + letter-spacing: -0.03em; + line-height: 1.1; +} + +.kpi-card .kpi-value .kpi-unit { + font-size: 14px; + font-weight: 400; + opacity: 0.45; +} + +.kpi-card .kpi-footer { + margin-top: 16px; + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + display: flex; + align-items: center; + gap: 4px; +} + +/* Main 8:4 Grid */ +.dashboard-grid { + display: grid; + grid-template-columns: 2fr 1fr; + gap: 24px; +} + +@media (max-width: 1024px) { + .dashboard-grid { + grid-template-columns: 1fr; + } +} + +.dash-col { + display: flex; + flex-direction: column; + gap: 24px; +} + +/* Section Cards */ +.dash-section { + padding: 24px; + border-radius: 8px; +} + +.dash-section-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 24px; +} + +.dash-section-title { + font-size: 17px; + font-weight: 700; + margin: 0; + display: flex; + align-items: center; + gap: 8px; +} + +/* Low Stock */ +.low-stock-list { + display: flex; + flex-direction: column; + gap: 8px; +} + +.low-stock-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 16px; + border-radius: 6px; + cursor: pointer; + transition: background 0.15s ease; +} + +.low-stock-item:hover { + filter: brightness(1.15); +} + +.low-stock-left { + display: flex; + align-items: center; + gap: 14px; + min-width: 0; + flex: 1; +} + +.low-stock-color-dot { + width: 42px; + height: 42px; + border-radius: 6px; + flex-shrink: 0; +} + +.low-stock-info h4 { + font-size: 13px; + font-weight: 600; + margin: 0; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.low-stock-info p { + font-size: 11px; + opacity: 0.4; + margin: 2px 0 0 0; +} + +.low-stock-right { + text-align: right; + flex-shrink: 0; + margin-left: 16px; +} + +.low-stock-weight { + font-size: 13px; + font-weight: 700; +} + +.low-stock-weight .total { + font-weight: 400; + opacity: 0.4; +} + +.low-stock-bar { + width: 128px; + height: 5px; + border-radius: 3px; + overflow: hidden; + margin-top: 8px; + margin-left: auto; +} + +.low-stock-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s ease; +} + +/* Material Bars */ +.material-list { + display: flex; + flex-direction: column; + gap: 20px; +} + +.material-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; +} + +.material-name { + font-size: 13px; + font-weight: 600; +} + +.material-weight { + font-size: 13px; + font-weight: 700; +} + +.material-bar { + height: 7px; + border-radius: 4px; + overflow: hidden; +} + +.material-bar-fill { + height: 100%; + border-radius: 4px; + transition: width 0.3s ease; +} + +/* Timeline */ +.timeline-list { + display: flex; + flex-direction: column; +} + +.timeline-item { + position: relative; + padding-bottom: 24px; + padding-left: 28px; + margin-left: 6px; + border-left: 1px solid rgba(255, 255, 255, 0.06); + cursor: pointer; +} + +.timeline-item:last-child { + border-left-color: transparent; + padding-bottom: 0; +} + +.timeline-item:hover .timeline-name { + opacity: 0.7; +} + +.timeline-dot { + position: absolute; + left: -5px; + top: 0; + width: 9px; + height: 9px; + border-radius: 50%; +} + +.timeline-dot.active { + box-shadow: 0 0 10px currentColor; +} + +.timeline-time { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: -0.01em; + opacity: 0.35; +} + +.timeline-name { + font-size: 13px; + font-weight: 600; + margin-top: 3px; + transition: opacity 0.15s; +} + +.timeline-detail { + font-size: 11px; + opacity: 0.4; + margin-top: 2px; +} + +/* Location List */ +.location-list { + display: flex; + flex-direction: column; + gap: 6px; +} + +.location-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 16px; + border-radius: 6px; + cursor: pointer; + transition: background 0.15s ease; +} + +.location-item:hover { + filter: brightness(1.15); +} + +.location-name { + font-size: 13px; + font-weight: 500; +} + +.location-badge { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + padding: 3px 10px; + border-radius: 4px; + letter-spacing: 0.02em; +} + +/* Empty state */ +.dash-empty { + text-align: center; + padding: 32px; + opacity: 0.3; + font-size: 13px; +} diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index ec0ab95bb..dd332517f 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -1,29 +1,53 @@ -import { FileOutlined, HighlightOutlined, PlusOutlined, UnorderedListOutlined, UserOutlined } from "@ant-design/icons"; -import { useList, useTranslate } from "@refinedev/core"; -import { Card, Col, Row, Statistic, theme } from "antd"; -import { Content } from "antd/es/layout/layout"; -import Title from "antd/es/typography/Title"; +import { + DatabaseOutlined, + EnvironmentOutlined, + ExperimentOutlined, + HighlightOutlined, + PlusOutlined, + ShopOutlined, + ShoppingOutlined, + WarningOutlined, +} from "@ant-design/icons"; +import { useList, useNavigation, useTranslate } from "@refinedev/core"; +import { theme } from "antd"; import dayjs from "dayjs"; +import relativeTime from "dayjs/plugin/relativeTime"; import utc from "dayjs/plugin/utc"; -import { ReactNode } from "react"; import { Trans } from "react-i18next"; -import { Link } from "react-router"; -import Logo from "../../icon.svg?react"; +import { Link, useNavigate } from "react-router"; +import { formatWeight } from "../../utils/parsing"; +import { useCurrencyFormatter } from "../../utils/settings"; +import { IFilament } from "../filaments/model"; import { ISpool } from "../spools/model"; +import "./home.css"; dayjs.extend(utc); +dayjs.extend(relativeTime); const { useToken } = theme; +// Dark surface palette — works on top of the app's existing dark background +const S = { + lowest: "#1a1a1a", + low: "#1f1f1f", + base: "#252525", + high: "#2a2a2a", + highest: "#313131", +}; + export const Home = () => { const { token } = useToken(); const t = useTranslate(); + const navigate = useNavigate(); + const { showUrl } = useNavigation(); + const currencyFormatter = useCurrencyFormatter(); - const spools = useList({ + const spoolsAll = useList({ resource: "spool", - pagination: { pageSize: 1 }, + pagination: { mode: "off" }, + meta: { queryParams: { allow_archived: false } }, }); - const filaments = useList({ + const filaments = useList({ resource: "filament", pagination: { pageSize: 1 }, }); @@ -32,94 +56,345 @@ export const Home = () => { pagination: { pageSize: 1 }, }); - const hasSpools = !spools.result || spools.result.data.length > 0; - - const ResourceStatsCard = (props: { loading: boolean; value: number; resource: string; icon: ReactNode }) => ( - - - - , - - - , - ]} - > - - - - ); + const allSpools = spoolsAll.result?.data ?? []; + const hasSpools = allSpools.length > 0; + const isLoading = spoolsAll.query.isLoading; + + // --- Calculations --- + const totalRemainingWeight = allSpools.reduce((sum, s) => sum + (s.remaining_weight ?? 0), 0); + const totalValue = allSpools.reduce((sum, s) => sum + (s.price ?? 0), 0); + + const lowStockSpools = allSpools + .filter((s) => { + const total = s.initial_weight ?? s.filament.weight ?? 1000; + const remaining = s.remaining_weight ?? total; + return remaining / total < 0.15; + }) + .sort((a, b) => { + const pctA = (a.remaining_weight ?? 0) / (a.initial_weight ?? a.filament.weight ?? 1000); + const pctB = (b.remaining_weight ?? 0) / (b.initial_weight ?? b.filament.weight ?? 1000); + return pctA - pctB; + }); + + const recentSpools = [...allSpools] + .filter((s) => s.last_used) + .sort((a, b) => dayjs(b.last_used).valueOf() - dayjs(a.last_used).valueOf()) + .slice(0, 5); + + const materialMap: Record = {}; + allSpools.forEach((s) => { + const mat = s.filament.material ?? "Unknown"; + if (!materialMap[mat]) materialMap[mat] = { count: 0, weight: 0 }; + materialMap[mat].count++; + materialMap[mat].weight += s.remaining_weight ?? 0; + }); + const materialBreakdown = Object.entries(materialMap).sort((a, b) => b[1].weight - a[1].weight); + + const locationMap: Record = {}; + allSpools.forEach((s) => { + const loc = s.location || t("locations.no_location"); + locationMap[loc] = (locationMap[loc] ?? 0) + 1; + }); + const locationBreakdown = Object.entries(locationMap).sort((a, b) => b[1] - a[1]); + + const vendorCount: Record = {}; + allSpools.forEach((s) => { + const name = s.filament.vendor && "name" in s.filament.vendor ? s.filament.vendor.name : "?"; + vendorCount[name] = (vendorCount[name] ?? 0) + 1; + }); + const topVendor = Object.entries(vendorCount).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "-"; + + // --- Helpers --- + function getColorHex(spool: ISpool): string { + return "#" + (spool.filament.color_hex ?? "555555").replace("#", ""); + } + + function getSpoolName(spool: ISpool): string { + if (spool.filament.vendor && "name" in spool.filament.vendor) { + return `${spool.filament.vendor.name} - ${spool.filament.name}`; + } + return spool.filament.name ?? spool.filament.id.toString(); + } + + function getWeightPct(spool: ISpool): number { + const total = spool.initial_weight ?? spool.filament.weight ?? 1000; + const remaining = spool.remaining_weight ?? total; + return Math.max(0, Math.min(100, (remaining / total) * 100)); + } + + const matColors: Record = { + PLA: "#81ecff", + "PLA+": "#00e3fd", + PETG: "#6ded00", + ABS: "#ff7350", + "ABS+": "#ff9070", + ASA: "#eb2f96", + TPU: "#b388ff", + "TPU 95A": "#b388ff", + "PETG-CF": "#00bcd4", + nGen: "#ff5252", + }; + + if (isLoading) { + return
Loading...
; + } + + if (!hasSpools) { + return ( +
+

{t("home.welcome")}

+

+ }} /> +

+
+ ); + } return ( - - - <div + <div className="dashboard"> + {/* Header */} + <div className="dashboard-header"> + <div> + <h2>{t("home.home")}</h2> + <p className="dash-subtitle">{t("home.telemetry_subtitle") || "Real-time status of your filament inventory."}</p> + </div> + <button + className="dash-new-btn" + onClick={() => navigate("/spool/create")} style={{ - display: "inline-block", - height: "1.5em", - marginRight: "0.5em", + background: `linear-gradient(135deg, ${token.colorPrimary}, ${token.colorPrimaryActive})`, + color: "#000", }} > - <Logo /> + <PlusOutlined /> {t("spool.titles.create") || "New Spool"} + </button> + </div> + + {/* KPI Cards */} + <div className="kpi-grid"> + <div className="kpi-card" style={{ background: S.low }}> + <DatabaseOutlined className="kpi-bg-icon" /> + <div className="kpi-label">{t("spool.spool")}</div> + <div className="kpi-value">{spoolsAll.result?.total ?? 0}</div> + <div className="kpi-footer" style={{ color: "#6ded00" }}> + <span>+{allSpools.filter((s) => dayjs(s.registered).isAfter(dayjs().subtract(30, "day"))).length} THIS MONTH</span> + </div> + </div> + + <div className="kpi-card" style={{ background: S.low }}> + <HighlightOutlined className="kpi-bg-icon" /> + <div className="kpi-label">{t("filament.filament")}</div> + <div className="kpi-value">{filaments.result?.total ?? 0}</div> + <div className="kpi-footer" style={{ color: "#00e3fd" }}> + <span>ALL SYNCED</span> + </div> + </div> + + <div className="kpi-card" style={{ background: S.low }}> + <ShopOutlined className="kpi-bg-icon" /> + <div className="kpi-label">{t("vendor.vendor")}</div> + <div className="kpi-value">{vendors.result?.total ?? 0}</div> + <div className="kpi-footer" style={{ opacity: 0.4 }}> + TOP: {topVendor.toUpperCase()} + </div> + </div> + + <div className="kpi-card" style={{ background: S.low }}> + <ShoppingOutlined className="kpi-bg-icon" /> + <div className="kpi-label">{t("home.total_weight")}</div> + <div className="kpi-value"> + {formatWeight(totalRemainingWeight, 1).split(" ")[0]}{" "} + <span className="kpi-unit">{formatWeight(totalRemainingWeight, 1).split(" ")[1]}</span> + </div> + <div className="kpi-footer" style={{ color: lowStockSpools.length > 0 ? "#ff716c" : undefined, opacity: lowStockSpools.length > 0 ? 1 : 0.4 }}> + {lowStockSpools.length > 0 ? ( + <><WarningOutlined /> {lowStockSpools.length} {t("home.low_stock").toUpperCase()}</> + ) : ( + <span>{t("home.total_value")}: {currencyFormatter.format(totalValue)}</span> + )} + </div> + </div> + </div> + + {/* Main 2:1 Grid */} + <div className="dashboard-grid"> + {/* Left Column — Low Stock + Materials */} + <div className="dash-col"> + {/* Low Stock */} + <div className="dash-section" style={{ background: S.low }}> + <div className="dash-section-header"> + <h3 className="dash-section-title"> + <WarningOutlined style={{ color: "#ff716c" }} /> + {t("home.low_stock")} + </h3> + </div> + {lowStockSpools.length === 0 ? ( + <div className="dash-empty">{t("home.all_stocked")}</div> + ) : ( + <div className="low-stock-list"> + {lowStockSpools.map((spool) => { + const pct = getWeightPct(spool); + const remaining = spool.remaining_weight ?? 0; + const total = spool.initial_weight ?? spool.filament.weight ?? 1000; + const barColor = pct <= 5 ? "#ff716c" : "#d7383b"; + const hex = getColorHex(spool); + + return ( + <div + key={spool.id} + className="low-stock-item" + style={{ background: S.lowest }} + onClick={() => navigate(showUrl("spool", spool.id))} + > + <div className="low-stock-left"> + <div + className="low-stock-color-dot" + style={{ backgroundColor: hex, boxShadow: `0 0 14px ${hex}50` }} + /> + <div className="low-stock-info"> + <h4>{getSpoolName(spool)}</h4> + <p>Material: {spool.filament.material ?? "?"}</p> + </div> + </div> + <div className="low-stock-right"> + <div className="low-stock-weight" style={{ color: barColor }}> + {formatWeight(remaining, 0)} <span className="total">/ {formatWeight(total, 0)}</span> + </div> + <div className="low-stock-bar" style={{ background: S.highest }}> + <div + className="low-stock-bar-fill" + style={{ + width: `${Math.max(pct, 1)}%`, + backgroundColor: barColor, + boxShadow: `0 0 8px ${barColor}80`, + }} + /> + </div> + </div> + </div> + ); + })} + </div> + )} + </div> + + {/* Material Breakdown */} + <div className="dash-section" style={{ background: S.low }}> + <div className="dash-section-header"> + <h3 className="dash-section-title"> + <ExperimentOutlined /> + {t("home.by_material")} + </h3> + </div> + <div className="material-list"> + {materialBreakdown.map(([material, data]) => { + const maxWeight = materialBreakdown[0]?.[1].weight || 1; + const pct = (data.weight / maxWeight) * 100; + const color = matColors[material] ?? "#81ecff"; + return ( + <div key={material}> + <div className="material-header"> + <span className="material-name">{material}</span> + <span className="material-weight">{formatWeight(data.weight, 0)}</span> + </div> + <div className="material-bar" style={{ background: S.highest }}> + <div + className="material-bar-fill" + style={{ + width: `${pct}%`, + backgroundColor: color, + boxShadow: `0 0 12px ${color}40`, + }} + /> + </div> + </div> + ); + })} + </div> + </div> + </div> + + {/* Right Column — Recently Used + Locations */} + <div className="dash-col"> + {/* Recently Used */} + <div className="dash-section" style={{ background: S.low }}> + <div className="dash-section-header"> + <h3 className="dash-section-title">{t("home.recently_used")}</h3> + </div> + {recentSpools.length === 0 ? ( + <div className="dash-empty">{t("home.no_recent")}</div> + ) : ( + <div className="timeline-list"> + {recentSpools.map((spool, idx) => { + const isFirst = idx === 0; + return ( + <div + key={spool.id} + className="timeline-item" + onClick={() => navigate(showUrl("spool", spool.id))} + > + <div + className={"timeline-dot" + (isFirst ? " active" : "")} + style={{ + backgroundColor: isFirst ? "#81ecff" : "rgba(255,255,255,0.12)", + color: isFirst ? "#81ecff" : undefined, + }} + /> + <div> + <div className="timeline-time">{dayjs(spool.last_used).fromNow()}</div> + <div className="timeline-name">{getSpoolName(spool)}</div> + <div className="timeline-detail"> + {spool.filament.material ?? ""} · {formatWeight(spool.remaining_weight ?? 0, 0)} · {spool.location || t("locations.no_location")} + </div> + </div> + </div> + ); + })} + </div> + )} + </div> + + {/* By Location */} + <div className="dash-section" style={{ background: S.low }}> + <div className="dash-section-header"> + <h3 className="dash-section-title"> + <EnvironmentOutlined /> + {t("home.by_location")} + </h3> + </div> + <div className="location-list"> + {locationBreakdown.map(([location, count], idx) => { + let badgeBg: string; + let badgeColor: string; + if (idx === 0) { + badgeBg = "rgba(129, 236, 255, 0.1)"; + badgeColor = "#00e3fd"; + } else if (idx < 3) { + badgeBg = "rgba(109, 237, 0, 0.08)"; + badgeColor = "#6ded00"; + } else { + badgeBg = "rgba(255, 255, 255, 0.04)"; + badgeColor = "rgba(255, 255, 255, 0.35)"; + } + return ( + <div + key={location} + className="location-item" + style={{ background: S.high }} + onClick={() => navigate("/locations")} + > + <span className="location-name">{location}</span> + <span className="location-badge" style={{ background: badgeBg, color: badgeColor }}> + {count} {t("spool.spool")} + </span> + </div> + ); + })} + </div> + </div> </div> - Spoolman - - - } - /> - } - /> - } - /> - - {!hasSpools && ( - <> -

{t("home.welcome")}

-

- , - }} - /> -

- - )} -
+
+
); }; From 2d94ba1e1a5718d72300dde1001d71191724e02b Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:34:45 +0100 Subject: [PATCH 05/19] Add missing telemetry_subtitle i18n key to all 28 locales --- client/public/locales/cs/common.json | 3 ++- client/public/locales/da/common.json | 3 ++- client/public/locales/de/common.json | 3 ++- client/public/locales/el/common.json | 3 ++- client/public/locales/en/common.json | 3 ++- client/public/locales/es/common.json | 3 ++- client/public/locales/et/common.json | 3 ++- client/public/locales/fa/common.json | 3 ++- client/public/locales/fr/common.json | 3 ++- client/public/locales/hi-Latn/common.json | 3 ++- client/public/locales/hu/common.json | 3 ++- client/public/locales/it/common.json | 3 ++- client/public/locales/ja/common.json | 3 ++- client/public/locales/lt/common.json | 3 ++- client/public/locales/nb-NO/common.json | 3 ++- client/public/locales/nl/common.json | 3 ++- client/public/locales/pl/common.json | 3 ++- client/public/locales/pt-BR/common.json | 3 ++- client/public/locales/pt/common.json | 3 ++- client/public/locales/ro/common.json | 3 ++- client/public/locales/ru/common.json | 3 ++- client/public/locales/sv/common.json | 3 ++- client/public/locales/ta/common.json | 3 ++- client/public/locales/th/common.json | 3 ++- client/public/locales/tr/common.json | 3 ++- client/public/locales/uk/common.json | 3 ++- client/public/locales/zh-Hant/common.json | 3 ++- client/public/locales/zh/common.json | 3 ++- 28 files changed, 56 insertions(+), 28 deletions(-) diff --git a/client/public/locales/cs/common.json b/client/public/locales/cs/common.json index 84f35b75e..919de26ba 100644 --- a/client/public/locales/cs/common.json +++ b/client/public/locales/cs/common.json @@ -360,7 +360,8 @@ "recently_used": "Naposledy použité", "no_recent": "Žádné nedávno použité cívky", "by_material": "Podle materiálu", - "by_location": "Podle umístění" + "by_location": "Podle umístění", + "telemetry_subtitle": "Stav vašeho inventáře filamentů v reálném čase." }, "settings": { "extra_fields": { diff --git a/client/public/locales/da/common.json b/client/public/locales/da/common.json index 7fabc931a..4abefc7f1 100644 --- a/client/public/locales/da/common.json +++ b/client/public/locales/da/common.json @@ -324,7 +324,8 @@ "recently_used": "Senest brugt", "no_recent": "Ingen nyligt brugte spoler", "by_material": "Efter materiale", - "by_location": "Efter placering" + "by_location": "Efter placering", + "telemetry_subtitle": "Realtidsstatus for dit filamentlager." }, "settings": { "header": "Indstillinger", diff --git a/client/public/locales/de/common.json b/client/public/locales/de/common.json index d04eb1df2..5013c9fac 100644 --- a/client/public/locales/de/common.json +++ b/client/public/locales/de/common.json @@ -359,7 +359,8 @@ "recently_used": "Zuletzt verwendet", "no_recent": "Keine kürzlich verwendeten Spulen", "by_material": "Nach Material", - "by_location": "Nach Standort" + "by_location": "Nach Standort", + "telemetry_subtitle": "Echtzeitstatus deines Filament-Inventars." }, "settings": { "header": "Einstellungen", diff --git a/client/public/locales/el/common.json b/client/public/locales/el/common.json index faf308196..aaf101942 100644 --- a/client/public/locales/el/common.json +++ b/client/public/locales/el/common.json @@ -296,7 +296,8 @@ "recently_used": "Πρόσφατα χρησιμοποιημένα", "no_recent": "Δεν υπάρχουν πρόσφατα χρησιμοποιημένες κουβαρίστρες", "by_material": "Ανά υλικό", - "by_location": "Ανά τοποθεσία" + "by_location": "Ανά τοποθεσία", + "telemetry_subtitle": "Κατάσταση σε πραγματικό χρόνο του αποθέματος νημάτων σας." }, "settings": { "extra_fields": { diff --git a/client/public/locales/en/common.json b/client/public/locales/en/common.json index ec9255c81..c95ca9bba 100644 --- a/client/public/locales/en/common.json +++ b/client/public/locales/en/common.json @@ -301,7 +301,8 @@ "recently_used": "Recently Used", "no_recent": "No recently used spools", "by_material": "By Material", - "by_location": "By Location" + "by_location": "By Location", + "telemetry_subtitle": "Real-time status of your filament inventory." }, "help": { "help": "Help", diff --git a/client/public/locales/es/common.json b/client/public/locales/es/common.json index 9820c9493..21b28ff1c 100644 --- a/client/public/locales/es/common.json +++ b/client/public/locales/es/common.json @@ -360,7 +360,8 @@ "recently_used": "Usadas recientemente", "no_recent": "No hay bobinas usadas recientemente", "by_material": "Por material", - "by_location": "Por ubicación" + "by_location": "Por ubicación", + "telemetry_subtitle": "Estado en tiempo real de tu inventario de filamento." }, "settings": { "header": "Ajustes", diff --git a/client/public/locales/et/common.json b/client/public/locales/et/common.json index c8d05bb53..414e4919f 100644 --- a/client/public/locales/et/common.json +++ b/client/public/locales/et/common.json @@ -71,7 +71,8 @@ "recently_used": "Hiljuti kasutatud", "no_recent": "Hiljuti kasutatud poole pole", "by_material": "Materjali järgi", - "by_location": "Asukoha järgi" + "by_location": "Asukoha järgi", + "telemetry_subtitle": "Teie filamendi varude reaalajas olek." }, "locations": { "error_empty": "Nimi ei tohi olla tühi", diff --git a/client/public/locales/fa/common.json b/client/public/locales/fa/common.json index bc116d70b..2edfb7e02 100644 --- a/client/public/locales/fa/common.json +++ b/client/public/locales/fa/common.json @@ -296,7 +296,8 @@ "recently_used": "اخیراً استفاده شده", "no_recent": "قرقره‌ای اخیراً استفاده نشده", "by_material": "بر اساس مواد", - "by_location": "بر اساس مکان" + "by_location": "بر اساس مکان", + "telemetry_subtitle": "وضعیت بلادرنگ موجودی فیلامنت شما." }, "help": { "help": "راهنما", diff --git a/client/public/locales/fr/common.json b/client/public/locales/fr/common.json index 95199ee2a..d19446a80 100644 --- a/client/public/locales/fr/common.json +++ b/client/public/locales/fr/common.json @@ -359,7 +359,8 @@ "recently_used": "Utilisées récemment", "no_recent": "Aucune bobine utilisée récemment", "by_material": "Par matériau", - "by_location": "Par emplacement" + "by_location": "Par emplacement", + "telemetry_subtitle": "État en temps réel de votre inventaire de filament." }, "settings": { "settings": "Paramètres", diff --git a/client/public/locales/hi-Latn/common.json b/client/public/locales/hi-Latn/common.json index 722d0226f..e9312fef0 100644 --- a/client/public/locales/hi-Latn/common.json +++ b/client/public/locales/hi-Latn/common.json @@ -22,7 +22,8 @@ "recently_used": "Haal hi mein istemal ki gayi", "no_recent": "Koi haal hi mein istemal ki gayi spool nahi", "by_material": "Material ke anusaar", - "by_location": "Jagah ke anusaar" + "by_location": "Jagah ke anusaar", + "telemetry_subtitle": "Aapke filament inventory ki real-time sthiti." }, "locations": { "error_empty": "Naam khaali nahi ho sakta", diff --git a/client/public/locales/hu/common.json b/client/public/locales/hu/common.json index 6df77e01f..d2cb57d96 100644 --- a/client/public/locales/hu/common.json +++ b/client/public/locales/hu/common.json @@ -285,7 +285,8 @@ "recently_used": "Nemrég használt", "no_recent": "Nincsenek nemrég használt orsók", "by_material": "Anyag szerint", - "by_location": "Hely szerint" + "by_location": "Hely szerint", + "telemetry_subtitle": "A filament készleted valós idejű állapota." }, "help": { "resources": { diff --git a/client/public/locales/it/common.json b/client/public/locales/it/common.json index 82ad269d1..0505e9348 100644 --- a/client/public/locales/it/common.json +++ b/client/public/locales/it/common.json @@ -360,7 +360,8 @@ "recently_used": "Usate di recente", "no_recent": "Nessuna bobina usata di recente", "by_material": "Per materiale", - "by_location": "Per posizione" + "by_location": "Per posizione", + "telemetry_subtitle": "Stato in tempo reale del tuo inventario di filamento." }, "settings": { "settings": "Configurazione", diff --git a/client/public/locales/ja/common.json b/client/public/locales/ja/common.json index 6c05eafe6..0a97ce838 100644 --- a/client/public/locales/ja/common.json +++ b/client/public/locales/ja/common.json @@ -401,6 +401,7 @@ "recently_used": "最近使用", "no_recent": "最近使用したスプールはありません", "by_material": "素材別", - "by_location": "場所別" + "by_location": "場所別", + "telemetry_subtitle": "フィラメント在庫のリアルタイムステータス。" } } diff --git a/client/public/locales/lt/common.json b/client/public/locales/lt/common.json index c9ac66f43..f8c068b3a 100644 --- a/client/public/locales/lt/common.json +++ b/client/public/locales/lt/common.json @@ -302,7 +302,8 @@ "recently_used": "Neseniai naudotos", "no_recent": "Nėra neseniai naudotų ričių", "by_material": "Pagal medžiagą", - "by_location": "Pagal vietą" + "by_location": "Pagal vietą", + "telemetry_subtitle": "Jūsų filamento atsargų būsena realiu laiku." }, "help": { "help": "Pagalba", diff --git a/client/public/locales/nb-NO/common.json b/client/public/locales/nb-NO/common.json index 117c4d74e..e3830d824 100644 --- a/client/public/locales/nb-NO/common.json +++ b/client/public/locales/nb-NO/common.json @@ -350,7 +350,8 @@ "recently_used": "Nylig brukt", "no_recent": "Ingen nylig brukte spoler", "by_material": "Etter materiale", - "by_location": "Etter plassering" + "by_location": "Etter plassering", + "telemetry_subtitle": "Sanntidsstatus for filamentlageret ditt." }, "help": { "help": "Hjelp", diff --git a/client/public/locales/nl/common.json b/client/public/locales/nl/common.json index a14853b4e..8a60ecee7 100644 --- a/client/public/locales/nl/common.json +++ b/client/public/locales/nl/common.json @@ -357,7 +357,8 @@ "recently_used": "Recent gebruikt", "no_recent": "Geen recent gebruikte spoelen", "by_material": "Per materiaal", - "by_location": "Per locatie" + "by_location": "Per locatie", + "telemetry_subtitle": "Realtime status van je filamentvoorraad." }, "settings": { "header": "Instellingen", diff --git a/client/public/locales/pl/common.json b/client/public/locales/pl/common.json index 4eda705d1..c5c124755 100644 --- a/client/public/locales/pl/common.json +++ b/client/public/locales/pl/common.json @@ -360,7 +360,8 @@ "recently_used": "Ostatnio używane", "no_recent": "Brak ostatnio używanych szpul", "by_material": "Wg materiału", - "by_location": "Wg lokalizacji" + "by_location": "Wg lokalizacji", + "telemetry_subtitle": "Stan zapasów filamentu w czasie rzeczywistym." }, "settings": { "settings": "Ustawienia", diff --git a/client/public/locales/pt-BR/common.json b/client/public/locales/pt-BR/common.json index 814a6be37..817206fa1 100644 --- a/client/public/locales/pt-BR/common.json +++ b/client/public/locales/pt-BR/common.json @@ -302,7 +302,8 @@ "recently_used": "Usadas recentemente", "no_recent": "Nenhuma bobina usada recentemente", "by_material": "Por material", - "by_location": "Por localização" + "by_location": "Por localização", + "telemetry_subtitle": "Status em tempo real do seu inventário de filamento." }, "help": { "help": "Ajuda", diff --git a/client/public/locales/pt/common.json b/client/public/locales/pt/common.json index 01728fdce..d46ba08e5 100644 --- a/client/public/locales/pt/common.json +++ b/client/public/locales/pt/common.json @@ -301,7 +301,8 @@ "recently_used": "Usadas recentemente", "no_recent": "Sem bobinas usadas recentemente", "by_material": "Por material", - "by_location": "Por localização" + "by_location": "Por localização", + "telemetry_subtitle": "Estado em tempo real do seu inventário de filamento." }, "help": { "help": "Ajuda", diff --git a/client/public/locales/ro/common.json b/client/public/locales/ro/common.json index 82171b76a..ceeae187e 100644 --- a/client/public/locales/ro/common.json +++ b/client/public/locales/ro/common.json @@ -345,7 +345,8 @@ "recently_used": "Utilizate recent", "no_recent": "Nicio bobină utilizată recent", "by_material": "După material", - "by_location": "După locație" + "by_location": "După locație", + "telemetry_subtitle": "Starea în timp real a inventarului de filament." }, "scanner": { "error": { diff --git a/client/public/locales/ru/common.json b/client/public/locales/ru/common.json index 4f744b5b4..1d728ee04 100644 --- a/client/public/locales/ru/common.json +++ b/client/public/locales/ru/common.json @@ -360,7 +360,8 @@ "recently_used": "Недавно использованные", "no_recent": "Нет недавно использованных катушек", "by_material": "По материалу", - "by_location": "По расположению" + "by_location": "По расположению", + "telemetry_subtitle": "Состояние запасов филамента в реальном времени." }, "settings": { "extra_fields": { diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index 16f693324..da7a12af2 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -301,7 +301,8 @@ "recently_used": "Nyligen använda", "no_recent": "Inga nyligen använda spolar", "by_material": "Per material", - "by_location": "Per plats" + "by_location": "Per plats", + "telemetry_subtitle": "Realtidsstatus för ditt filamentlager." }, "table": { "actions": "Åtgärder" diff --git a/client/public/locales/ta/common.json b/client/public/locales/ta/common.json index 0ea2a6cf3..76a3bdafb 100644 --- a/client/public/locales/ta/common.json +++ b/client/public/locales/ta/common.json @@ -352,7 +352,8 @@ "recently_used": "சமீபத்தில் பயன்படுத்தியவை", "no_recent": "சமீபத்தில் பயன்படுத்திய ஸ்பூல்கள் இல்லை", "by_material": "பொருள் வாரியாக", - "by_location": "இடம் வாரியாக" + "by_location": "இடம் வாரியாக", + "telemetry_subtitle": "உங்கள் பிலமென்ட் சரக்குகளின் நிகழ்நேர நிலை." }, "table": { "actions": "செயல்கள்" diff --git a/client/public/locales/th/common.json b/client/public/locales/th/common.json index b85e01fda..43ba900f4 100644 --- a/client/public/locales/th/common.json +++ b/client/public/locales/th/common.json @@ -393,7 +393,8 @@ "recently_used": "ใช้ล่าสุด", "no_recent": "ไม่มีม้วนที่ใช้ล่าสุด", "by_material": "ตามวัสดุ", - "by_location": "ตามตำแหน่ง" + "by_location": "ตามตำแหน่ง", + "telemetry_subtitle": "สถานะแบบเรียลไทม์ของคลังเส้นพลาสติกของคุณ" }, "warnWhenUnsavedChanges": "คุณแน่ใจหรือไม่ว่าต้องการออก? คุณมีการเปลี่ยนแปลงที่ยังไม่ได้บันทึก", "table": { diff --git a/client/public/locales/tr/common.json b/client/public/locales/tr/common.json index a1a8dfeb1..d94068509 100644 --- a/client/public/locales/tr/common.json +++ b/client/public/locales/tr/common.json @@ -301,7 +301,8 @@ "recently_used": "Son kullanılan", "no_recent": "Son kullanılan makara yok", "by_material": "Malzemeye göre", - "by_location": "Konuma göre" + "by_location": "Konuma göre", + "telemetry_subtitle": "Filament envanterinizin gerçek zamanlı durumu." }, "help": { "help": "Yardım", diff --git a/client/public/locales/uk/common.json b/client/public/locales/uk/common.json index fc44b6664..91bc171e8 100644 --- a/client/public/locales/uk/common.json +++ b/client/public/locales/uk/common.json @@ -341,7 +341,8 @@ "recently_used": "Нещодавно використані", "no_recent": "Немає нещодавно використаних котушок", "by_material": "За матеріалом", - "by_location": "За розташуванням" + "by_location": "За розташуванням", + "telemetry_subtitle": "Стан запасів філаменту в реальному часі." }, "settings": { "header": "Налаштування", diff --git a/client/public/locales/zh-Hant/common.json b/client/public/locales/zh-Hant/common.json index 331b90a44..191fedbb1 100644 --- a/client/public/locales/zh-Hant/common.json +++ b/client/public/locales/zh-Hant/common.json @@ -332,7 +332,8 @@ "recently_used": "最近使用", "no_recent": "沒有最近使用的線軸", "by_material": "按材料", - "by_location": "按位置" + "by_location": "按位置", + "telemetry_subtitle": "耗材庫存即時狀態。" }, "help": { "help": "說明", diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index 6bf13fb5d..ebd22260b 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -347,7 +347,8 @@ "recently_used": "最近使用", "no_recent": "没有最近使用的线轴", "by_material": "按材料", - "by_location": "按位置" + "by_location": "按位置", + "telemetry_subtitle": "耗材库存实时状态。" }, "help": { "help": "帮助", From 48795d04ab2e100c855acb7811f17b8a5a8e6e31 Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:39:39 +0100 Subject: [PATCH 06/19] Fix dashboard to support light theme with dynamic surface colors Detect dark/light mode from theme tokens and switch surface palette accordingly instead of hardcoding dark colors. --- client/src/pages/home/index.tsx | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index dd332517f..f2db90c82 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -27,16 +27,13 @@ dayjs.extend(relativeTime); const { useToken } = theme; // Dark surface palette — works on top of the app's existing dark background -const S = { - lowest: "#1a1a1a", - low: "#1f1f1f", - base: "#252525", - high: "#2a2a2a", - highest: "#313131", -}; - export const Home = () => { const { token } = useToken(); + const isDark = token.colorBgBase !== "#fff" && token.colorBgBase !== "#ffffff"; + + const S = isDark + ? { lowest: "#1a1a1a", low: "#1f1f1f", base: "#252525", high: "#2a2a2a", highest: "#313131" } + : { lowest: "#ffffff", low: "#f7f7f7", base: "#f0f0f0", high: "#e8e8e8", highest: "#e0e0e0" }; const t = useTranslate(); const navigate = useNavigate(); const { showUrl } = useNavigation(); @@ -336,8 +333,8 @@ export const Home = () => {
@@ -373,8 +370,8 @@ export const Home = () => { badgeBg = "rgba(109, 237, 0, 0.08)"; badgeColor = "#6ded00"; } else { - badgeBg = "rgba(255, 255, 255, 0.04)"; - badgeColor = "rgba(255, 255, 255, 0.35)"; + badgeBg = isDark ? "rgba(255, 255, 255, 0.04)" : "rgba(0, 0, 0, 0.04)"; + badgeColor = isDark ? "rgba(255, 255, 255, 0.35)" : "rgba(0, 0, 0, 0.4)"; } return (
Date: Mon, 23 Mar 2026 01:41:07 +0100 Subject: [PATCH 07/19] Fix dashboard light theme readability Use darker, saturated colors for material bars and badges in light mode. Replace neon glow effects with subtle shadows. Adjust surface palette for proper contrast on light backgrounds. --- client/src/pages/home/index.tsx | 55 ++++++++++++++++++++------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index f2db90c82..a611bc765 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -33,7 +33,7 @@ export const Home = () => { const S = isDark ? { lowest: "#1a1a1a", low: "#1f1f1f", base: "#252525", high: "#2a2a2a", highest: "#313131" } - : { lowest: "#ffffff", low: "#f7f7f7", base: "#f0f0f0", high: "#e8e8e8", highest: "#e0e0e0" }; + : { lowest: "#f5f5f5", low: "#ffffff", base: "#fafafa", high: "#f0f0f0", highest: "#d9d9d9" }; const t = useTranslate(); const navigate = useNavigate(); const { showUrl } = useNavigation(); @@ -119,17 +119,30 @@ export const Home = () => { return Math.max(0, Math.min(100, (remaining / total) * 100)); } - const matColors: Record = { - PLA: "#81ecff", - "PLA+": "#00e3fd", - PETG: "#6ded00", - ABS: "#ff7350", - "ABS+": "#ff9070", - ASA: "#eb2f96", - TPU: "#b388ff", - "TPU 95A": "#b388ff", - "PETG-CF": "#00bcd4", - nGen: "#ff5252", + const matColors: Record = isDark + ? { + PLA: "#81ecff", + "PLA+": "#00e3fd", + PETG: "#6ded00", + ABS: "#ff7350", + "ABS+": "#ff9070", + ASA: "#eb2f96", + TPU: "#b388ff", + "TPU 95A": "#b388ff", + "PETG-CF": "#00bcd4", + nGen: "#ff5252", + } + : { + PLA: "#0891b2", + "PLA+": "#0e7490", + PETG: "#16a34a", + ABS: "#ea580c", + "ABS+": "#f97316", + ASA: "#c026d3", + TPU: "#7c3aed", + "TPU 95A": "#7c3aed", + "PETG-CF": "#0d9488", + nGen: "#dc2626", }; if (isLoading) { @@ -173,7 +186,7 @@ export const Home = () => {
{t("spool.spool")}
{spoolsAll.result?.total ?? 0}
-
+
+{allSpools.filter((s) => dayjs(s.registered).isAfter(dayjs().subtract(30, "day"))).length} THIS MONTH
@@ -182,7 +195,7 @@ export const Home = () => {
{t("filament.filament")}
{filaments.result?.total ?? 0}
-
+
ALL SYNCED
@@ -246,7 +259,7 @@ export const Home = () => {

{getSpoolName(spool)}

@@ -263,7 +276,7 @@ export const Home = () => { style={{ width: `${Math.max(pct, 1)}%`, backgroundColor: barColor, - boxShadow: `0 0 8px ${barColor}80`, + boxShadow: isDark ? `0 0 8px ${barColor}80` : "none", }} />
@@ -300,7 +313,7 @@ export const Home = () => { style={{ width: `${pct}%`, backgroundColor: color, - boxShadow: `0 0 12px ${color}40`, + boxShadow: isDark ? `0 0 12px ${color}40` : "none", }} />
@@ -364,11 +377,11 @@ export const Home = () => { let badgeBg: string; let badgeColor: string; if (idx === 0) { - badgeBg = "rgba(129, 236, 255, 0.1)"; - badgeColor = "#00e3fd"; + badgeBg = isDark ? "rgba(129, 236, 255, 0.1)" : "rgba(8, 145, 178, 0.1)"; + badgeColor = isDark ? "#00e3fd" : "#0891b2"; } else if (idx < 3) { - badgeBg = "rgba(109, 237, 0, 0.08)"; - badgeColor = "#6ded00"; + badgeBg = isDark ? "rgba(109, 237, 0, 0.08)" : "rgba(22, 163, 74, 0.1)"; + badgeColor = isDark ? "#6ded00" : "#16a34a"; } else { badgeBg = isDark ? "rgba(255, 255, 255, 0.04)" : "rgba(0, 0, 0, 0.04)"; badgeColor = isDark ? "rgba(255, 255, 255, 0.35)" : "rgba(0, 0, 0, 0.4)"; From 1370d91f1ab4eb4ff4218fdf88ad52bf5231804e Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:48:39 +0100 Subject: [PATCH 08/19] Move version and Ko-fi into sidebar, push settings/help to bottom Use ThemedSider render prop to split menu items: main nav at top, settings and help pushed to the bottom with a flex spacer and divider. Version info and Ko-fi tip button now live in the sidebar footer instead of the page footer. --- client/src/components/layout.tsx | 103 ++++++++++++++++++++----------- 1 file changed, 68 insertions(+), 35 deletions(-) diff --git a/client/src/components/layout.tsx b/client/src/components/layout.tsx index 1921819b0..182af87c5 100644 --- a/client/src/components/layout.tsx +++ b/client/src/components/layout.tsx @@ -1,62 +1,95 @@ import { ThemedLayout, ThemedSider, ThemedTitle } from "@refinedev/antd"; import { useTranslate } from "@refinedev/core"; -import { Button } from "antd"; -import { Footer } from "antd/es/layout/layout"; +import { Button, Divider, Menu } from "antd"; +import React from "react"; import Logo from "../icon.svg?react"; import { getBasePath } from "../utils/url"; import { Header } from "./header"; import { Version } from "./version"; -const SpoolmanFooter = () => { +const SiderFooter = ({ collapsed }: { collapsed: boolean }) => { const t = useTranslate(); + if (collapsed) { + return ( +
+
+ ); + } + return ( -
-
-
- {t("version")} -
-
- -
+
+
+ {t("version")}
-
+ +
); }; +// Make the sider menu a flex column so the spacer li can push items down +const siderMenuStyle = ` + .ant-layout-sider .ant-menu { + display: flex !important; + flex-direction: column !important; + } +`; + export const SpoolmanLayout = ({ children }: { children: React.ReactNode }) => ( + <> +
} Sider={() => ( } />} + render={({ items, logout, collapsed }) => { + const bottomKeys = ["/settings", "/help"]; + const mainItems: React.ReactNode[] = []; + const bottomItems: React.ReactNode[] = []; + + React.Children.forEach(items as React.ReactNode, (child) => { + if (!React.isValidElement(child)) return; + const key = String(child.key ?? ""); + if (bottomKeys.some((k) => key.includes(k))) { + bottomItems.push(child); + } else { + mainItems.push(child); + } + }); + + return ( + <> + {mainItems} +
  • + + {bottomItems} + {logout} + + + ); + }} /> )} - Footer={() => } > {children} + ); From 4ef02aa1d700d10427aea5bed163a053b53cc059 Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 01:51:21 +0100 Subject: [PATCH 09/19] Replace single new spool button with 3 quick-add icon buttons Add icon-only buttons with tooltips for creating spools, filaments, and manufacturers directly from the dashboard header. --- client/src/pages/home/index.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index a611bc765..78bacbf1b 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -9,7 +9,7 @@ import { WarningOutlined, } from "@ant-design/icons"; import { useList, useNavigation, useTranslate } from "@refinedev/core"; -import { theme } from "antd"; +import { Button, theme, Tooltip } from "antd"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import utc from "dayjs/plugin/utc"; @@ -168,16 +168,17 @@ export const Home = () => {

    {t("home.home")}

    {t("home.telemetry_subtitle") || "Real-time status of your filament inventory."}

  • - +
    + +
    {/* KPI Cards */} From 0010ee7b64a6df539f068c886af37eeea8029c14 Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 20:52:22 +0100 Subject: [PATCH 10/19] Add missing home.welcome and home.description i18n keys to 12 locales da, el, fa, hu, ja, nl, sv, th, uk, zh, zh-Hant were missing both keys; fr was missing only description. --- client/public/locales/da/common.json | 2 ++ client/public/locales/el/common.json | 2 ++ client/public/locales/fa/common.json | 2 ++ client/public/locales/fr/common.json | 1 + client/public/locales/hu/common.json | 2 ++ client/public/locales/ja/common.json | 2 ++ client/public/locales/nl/common.json | 2 ++ client/public/locales/sv/common.json | 2 ++ client/public/locales/th/common.json | 2 ++ client/public/locales/uk/common.json | 2 ++ client/public/locales/zh-Hant/common.json | 2 ++ client/public/locales/zh/common.json | 2 ++ 12 files changed, 23 insertions(+) diff --git a/client/public/locales/da/common.json b/client/public/locales/da/common.json index 4abefc7f1..53513528f 100644 --- a/client/public/locales/da/common.json +++ b/client/public/locales/da/common.json @@ -317,6 +317,8 @@ "kofi": "Donér på Ko-fi", "home": { "home": "Hjem", + "welcome": "Velkommen til din Spoolman-instans!", + "description": "Det ser ud til, at du endnu ikke har tilføjet nogen spoler. Se hjælpesiden for at komme i gang.", "total_weight": "Samlet beholdning", "total_value": "Værdi", "low_stock": "Lav beholdning", diff --git a/client/public/locales/el/common.json b/client/public/locales/el/common.json index aaf101942..5084be6e7 100644 --- a/client/public/locales/el/common.json +++ b/client/public/locales/el/common.json @@ -289,6 +289,8 @@ "kofi": "Φιλοδώρημα στο Ko-fi", "home": { "home": "Αρχική", + "welcome": "Καλώς ήρθατε στην εγκατάσταση Spoolman σας!", + "description": "Φαίνεται ότι δεν έχετε προσθέσει ακόμα κουβαρίστρες. Δείτε τη σελίδα βοήθειας για να ξεκινήσετε.", "total_weight": "Συνολικό απόθεμα", "total_value": "Αξία", "low_stock": "Χαμηλό απόθεμα", diff --git a/client/public/locales/fa/common.json b/client/public/locales/fa/common.json index 2edfb7e02..97ef8d98c 100644 --- a/client/public/locales/fa/common.json +++ b/client/public/locales/fa/common.json @@ -289,6 +289,8 @@ }, "home": { "home": "صفحه اصلی", + "welcome": "به نمونه Spoolman خود خوش آمدید!", + "description": "به نظر می‌رسد هنوز هیچ قرقره‌ای اضافه نکرده‌اید. برای شروع به صفحه راهنما مراجعه کنید.", "total_weight": "موجودی کل", "total_value": "ارزش", "low_stock": "موجودی کم", diff --git a/client/public/locales/fr/common.json b/client/public/locales/fr/common.json index d19446a80..636e7d8cb 100644 --- a/client/public/locales/fr/common.json +++ b/client/public/locales/fr/common.json @@ -352,6 +352,7 @@ "home": { "home": "Accueil", "welcome": "Bienvenue sur votre instance Spoolman !", + "description": "Il semble que vous n'ayez pas encore ajouté de bobines. Consultez la page d'aide pour commencer.", "total_weight": "Stock total", "total_value": "Valeur", "low_stock": "Stock faible", diff --git a/client/public/locales/hu/common.json b/client/public/locales/hu/common.json index d2cb57d96..127e74410 100644 --- a/client/public/locales/hu/common.json +++ b/client/public/locales/hu/common.json @@ -278,6 +278,8 @@ "kofi": "Tipp a Ko-fi-ra", "home": { "home": "Kezdő lap", + "welcome": "Üdvözöljük a Spoolman példányában!", + "description": "Úgy tűnik, még nem adott hozzá orsókat. Nézze meg a Súgó oldalt az induláshoz.", "total_weight": "Teljes készlet", "total_value": "Érték", "low_stock": "Alacsony készlet", diff --git a/client/public/locales/ja/common.json b/client/public/locales/ja/common.json index 0a97ce838..5114ef88c 100644 --- a/client/public/locales/ja/common.json +++ b/client/public/locales/ja/common.json @@ -394,6 +394,8 @@ }, "home": { "home": "ホーム", + "welcome": "Spoolmanへようこそ!", + "description": "まだスプールが追加されていないようです。始め方についてはヘルプページをご覧ください。", "total_weight": "総在庫", "total_value": "総額", "low_stock": "在庫不足", diff --git a/client/public/locales/nl/common.json b/client/public/locales/nl/common.json index 8a60ecee7..8f09ad596 100644 --- a/client/public/locales/nl/common.json +++ b/client/public/locales/nl/common.json @@ -350,6 +350,8 @@ "kofi": "Tip mij op Ko-fi", "home": { "home": "Thuis", + "welcome": "Welkom bij je Spoolman-installatie!", + "description": "Het lijkt erop dat je nog geen spoelen hebt toegevoegd. Bekijk de helppagina om aan de slag te gaan.", "total_weight": "Totale voorraad", "total_value": "Waarde", "low_stock": "Lage voorraad", diff --git a/client/public/locales/sv/common.json b/client/public/locales/sv/common.json index da7a12af2..a5cfe154b 100644 --- a/client/public/locales/sv/common.json +++ b/client/public/locales/sv/common.json @@ -294,6 +294,8 @@ }, "home": { "home": "Hem", + "welcome": "Välkommen till din Spoolman-instans!", + "description": "Det verkar som att du inte har lagt till några spolar ännu. Se hjälpsidan för att komma igång.", "total_weight": "Totalt lager", "total_value": "Värde", "low_stock": "Lågt lager", diff --git a/client/public/locales/th/common.json b/client/public/locales/th/common.json index 43ba900f4..8f8337f37 100644 --- a/client/public/locales/th/common.json +++ b/client/public/locales/th/common.json @@ -386,6 +386,8 @@ }, "home": { "home": "หน้าแรก", + "welcome": "ยินดีต้อนรับสู่ Spoolman ของคุณ!", + "description": "ดูเหมือนว่าคุณยังไม่ได้เพิ่มม้วนใดๆ ดูหน้าช่วยเหลือเพื่อเริ่มต้นใช้งาน", "total_weight": "สต็อกทั้งหมด", "total_value": "มูลค่า", "low_stock": "สต็อกต่ำ", diff --git a/client/public/locales/uk/common.json b/client/public/locales/uk/common.json index 91bc171e8..c00e4d834 100644 --- a/client/public/locales/uk/common.json +++ b/client/public/locales/uk/common.json @@ -334,6 +334,8 @@ "kofi": "Задонатити на Ko-fi", "home": { "home": "Домашня", + "welcome": "Ласкаво просимо до вашого екземпляра Spoolman!", + "description": "Схоже, що ви ще не додали жодної котушки. Перегляньте сторінку довідки, щоб дізнатися, як почати.", "total_weight": "Загальний запас", "total_value": "Вартість", "low_stock": "Низький запас", diff --git a/client/public/locales/zh-Hant/common.json b/client/public/locales/zh-Hant/common.json index 191fedbb1..3368f4332 100644 --- a/client/public/locales/zh-Hant/common.json +++ b/client/public/locales/zh-Hant/common.json @@ -325,6 +325,8 @@ }, "home": { "home": "主頁", + "welcome": "歡迎使用您的 Spoolman!", + "description": "看起來您還沒有添加任何線軸。請查看說明頁面以開始使用。", "total_weight": "總庫存", "total_value": "總價值", "low_stock": "庫存不足", diff --git a/client/public/locales/zh/common.json b/client/public/locales/zh/common.json index ebd22260b..a712281e0 100644 --- a/client/public/locales/zh/common.json +++ b/client/public/locales/zh/common.json @@ -340,6 +340,8 @@ "kofi": "在Ko-fi上给我打赏一点小费", "home": { "home": "主页", + "welcome": "欢迎使用您的 Spoolman!", + "description": "看起来您还没有添加任何线轴。请查看帮助页面以开始使用。", "total_weight": "总库存", "total_value": "总价值", "low_stock": "库存不足", From b5de5d4294e31b5a64102907e24336586b89dbbe Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 21:02:23 +0100 Subject: [PATCH 11/19] Add empty state hero with prominent add-spool button on home page Replaces the plain text empty state with a centered hero layout featuring a themed icon, welcome message, and a styled CTA button that navigates directly to spool creation. --- client/src/pages/home/home.css | 62 +++++++++++++++++++++++++++++++++ client/src/pages/home/index.tsx | 18 ++++++++-- 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/client/src/pages/home/home.css b/client/src/pages/home/home.css index 5fa3ef038..09eb6fb73 100644 --- a/client/src/pages/home/home.css +++ b/client/src/pages/home/home.css @@ -388,3 +388,65 @@ opacity: 0.3; font-size: 13px; } + +/* Empty Hero */ +.empty-hero { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + min-height: 60vh; + text-align: center; + gap: 0; +} + +.empty-hero-icon { + width: 88px; + height: 88px; + border-radius: 20px; + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 28px; + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3); +} + +.empty-hero-title { + font-size: 26px; + font-weight: 800; + letter-spacing: -0.03em; + margin: 0 0 8px 0; +} + +.empty-hero-desc { + font-size: 14px; + opacity: 0.5; + margin: 0 0 32px 0; + max-width: 400px; + line-height: 1.6; +} + +.empty-hero-desc a { + opacity: 1; + text-decoration: underline; +} + +.empty-hero-btn { + height: 48px !important; + padding: 0 32px !important; + font-size: 15px !important; + font-weight: 700 !important; + border-radius: 10px !important; + letter-spacing: 0.01em; + box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3); + transition: transform 0.15s ease, box-shadow 0.15s ease; +} + +.empty-hero-btn:hover { + transform: translateY(-2px); + box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4); +} + +.empty-hero-btn:active { + transform: scale(0.97); +} diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index 78bacbf1b..80a372275 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -151,11 +151,23 @@ export const Home = () => { if (!hasSpools) { return ( -
    -

    {t("home.welcome")}

    -

    +

    +
    + +
    +

    {t("home.welcome")}

    +

    }} />

    +
    ); } From 50d93b777743adbf45162c6bd6bbb7ac3459e671 Mon Sep 17 00:00:00 2001 From: RbVs Date: Mon, 23 Mar 2026 23:57:42 +0100 Subject: [PATCH 12/19] Refactor home page to non-scrollable tab layout with manufacturer tab - Replace scrollable dashboard grid with fixed viewport layout using tabs (Low Stock, By Material, By Manufacturer) and right column (Recently Used, By Location at 50/50 height) - Extract layout styles from inline +
    } Sider={() => ( @@ -91,5 +84,5 @@ export const SpoolmanLayout = ({ children }: { children: React.ReactNode }) => ( > {children} - +
    ); diff --git a/client/src/pages/home/home.css b/client/src/pages/home/home.css index 09eb6fb73..b76f2d13f 100644 --- a/client/src/pages/home/home.css +++ b/client/src/pages/home/home.css @@ -1,7 +1,13 @@ .dashboard { max-width: 1400px; + width: 100%; margin: 0 auto; padding: 0 4px; + display: flex; + flex-direction: column; + flex: 1; + min-height: 0; + overflow: hidden; } /* Header */ @@ -9,7 +15,8 @@ display: flex; justify-content: space-between; align-items: flex-end; - margin-bottom: 32px; + margin-bottom: 16px; + flex-shrink: 0; } .dashboard-header h2 { @@ -53,7 +60,8 @@ display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; - margin-bottom: 32px; + margin-bottom: 16px; + flex-shrink: 0; } @media (max-width: 1100px) { @@ -129,23 +137,64 @@ gap: 4px; } -/* Main 8:4 Grid */ -.dashboard-grid { +/* Main content area — fills remaining viewport */ +.dashboard-main { + flex: 1 1 0; + min-height: 0; display: grid; grid-template-columns: 2fr 1fr; - gap: 24px; + gap: 16px; + overflow: hidden; } @media (max-width: 1024px) { - .dashboard-grid { + .dashboard-main { grid-template-columns: 1fr; } } -.dash-col { +/* Left column: Tabs */ +.dashboard-main .ant-tabs { + display: flex; + flex-direction: column; + height: 100%; + min-height: 0; +} + +.dashboard-main .ant-tabs-content-holder { + flex: 1; + min-height: 0; + overflow: hidden; +} + +.dashboard-main .ant-tabs-content, +.dashboard-main .ant-tabs-tabpane-active { + height: 100%; +} + +.dashboard-main .ant-tabs-tabpane-active { + overflow-y: auto; +} + +.dashboard-main .ant-tabs-tabpane-active > .dash-section { + min-height: 100%; +} + +/* Right column */ +.dash-right-col { display: flex; flex-direction: column; - gap: 24px; + gap: 16px; + min-height: 0; + padding-top: 46px; +} + +.dash-right-section { + flex: 1; + min-height: 0; + overflow-y: auto; + padding: 24px; + border-radius: 8px; } /* Section Cards */ diff --git a/client/src/pages/home/index.tsx b/client/src/pages/home/index.tsx index 80a372275..e060a2477 100644 --- a/client/src/pages/home/index.tsx +++ b/client/src/pages/home/index.tsx @@ -9,7 +9,7 @@ import { WarningOutlined, } from "@ant-design/icons"; import { useList, useNavigation, useTranslate } from "@refinedev/core"; -import { Button, theme, Tooltip } from "antd"; +import { Button, Tabs, theme, Tooltip } from "antd"; import dayjs from "dayjs"; import relativeTime from "dayjs/plugin/relativeTime"; import utc from "dayjs/plugin/utc"; @@ -99,7 +99,8 @@ export const Home = () => { const name = s.filament.vendor && "name" in s.filament.vendor ? s.filament.vendor.name : "?"; vendorCount[name] = (vendorCount[name] ?? 0) + 1; }); - const topVendor = Object.entries(vendorCount).sort((a, b) => b[1] - a[1])[0]?.[0] ?? "-"; + const vendorBreakdown = Object.entries(vendorCount).sort((a, b) => b[1] - a[1]); + const topVendor = vendorBreakdown[0]?.[0] ?? "-"; // --- Helpers --- function getColorHex(spool: ISpool): string { @@ -239,108 +240,159 @@ export const Home = () => {
    - {/* Main 2:1 Grid */} -
    - {/* Left Column — Low Stock + Materials */} -
    - {/* Low Stock */} -
    -
    -

    - - {t("home.low_stock")} -

    -
    - {lowStockSpools.length === 0 ? ( -
    {t("home.all_stocked")}
    - ) : ( -
    - {lowStockSpools.map((spool) => { - const pct = getWeightPct(spool); - const remaining = spool.remaining_weight ?? 0; - const total = spool.initial_weight ?? spool.filament.weight ?? 1000; - const barColor = pct <= 5 ? "#ff716c" : "#d7383b"; - const hex = getColorHex(spool); + {/* Main content area */} +
    + {/* Left Column — Tabs */} + + {t("home.low_stock")} + + ), + children: ( +
    + {lowStockSpools.length === 0 ? ( +
    {t("home.all_stocked")}
    + ) : ( +
    + {lowStockSpools.map((spool) => { + const pct = getWeightPct(spool); + const remaining = spool.remaining_weight ?? 0; + const total = spool.initial_weight ?? spool.filament.weight ?? 1000; + const barColor = pct <= 5 ? "#ff716c" : "#d7383b"; + const hex = getColorHex(spool); - return ( -
    navigate(showUrl("spool", spool.id))} - > -
    -
    -
    -

    {getSpoolName(spool)}

    -

    Material: {spool.filament.material ?? "?"}

    -
    -
    -
    -
    - {formatWeight(remaining, 0)} / {formatWeight(total, 0)} -
    -
    + return (
    -
    -
    -
    - ); - })} -
    - )} -
    - - {/* Material Breakdown */} -
    -
    -

    - - {t("home.by_material")} -

    -
    -
    - {materialBreakdown.map(([material, data]) => { - const maxWeight = materialBreakdown[0]?.[1].weight || 1; - const pct = (data.weight / maxWeight) * 100; - const color = matColors[material] ?? "#81ecff"; - return ( -
    -
    - {material} - {formatWeight(data.weight, 0)} -
    -
    -
    + key={spool.id} + className="low-stock-item" + style={{ background: S.lowest }} + onClick={() => navigate(showUrl("spool", spool.id))} + > +
    +
    +
    +

    {getSpoolName(spool)}

    +

    Material: {spool.filament.material ?? "?"}

    +
    +
    +
    +
    + {formatWeight(remaining, 0)} / {formatWeight(total, 0)} +
    +
    +
    +
    +
    +
    + ); + })}
    + )} +
    + ), + }, + { + key: "materials", + label: ( + + {t("home.by_material")} + + ), + children: ( +
    +
    + {materialBreakdown.map(([material, data]) => { + const maxWeight = materialBreakdown[0]?.[1].weight || 1; + const pct = (data.weight / maxWeight) * 100; + const color = matColors[material] ?? "#81ecff"; + return ( +
    +
    + {material} + {formatWeight(data.weight, 0)} +
    +
    +
    +
    +
    + ); + })}
    - ); - })} -
    -
    -
    +
    + ), + }, + { + key: "vendors", + label: ( + + {t("home.by_vendor")} + + ), + children: ( +
    +
    + {vendorBreakdown.map(([vendor, count], idx) => { + const maxCount = vendorBreakdown[0]?.[1] || 1; + const pct = (count / maxCount) * 100; + let barColor: string; + if (idx === 0) { + barColor = isDark ? "#81ecff" : "#0891b2"; + } else if (idx < 3) { + barColor = isDark ? "#6ded00" : "#16a34a"; + } else { + barColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.2)"; + } + return ( +
    +
    + {vendor} + {count} {t("spool.spool")} +
    +
    +
    +
    +
    + ); + })} +
    +
    + ), + }, + ]} + /> {/* Right Column — Recently Used + Locations */} -
    - {/* Recently Used */} -
    +
    +

    {t("home.recently_used")}

    @@ -377,8 +429,7 @@ export const Home = () => { )}
    - {/* By Location */} -
    +

    From ab31aff08a44575a98c878d589518bce1f0e2156 Mon Sep 17 00:00:00 2001 From: RbVs Date: Tue, 24 Mar 2026 00:14:24 +0100 Subject: [PATCH 13/19] Fix mobile home page: enable scrolling, compact KPI cards, expand sections - Move mobile media query block to end of home.css to win CSS cascade over desktop rules (height:100%, flex:1, min-height:100% etc.) - KPI cards: 2x2 grid on mobile with compact padding/font sizes - Dashboard, tabs, and right column all use natural height on mobile - Refine wrapper switches to flex:none/height:auto on mobile so ant-layout-content handles scrolling - All sections (tab content, recently used, locations) expand to full content height instead of internal scrolling --- client/src/components/layout.css | 8 +++ client/src/pages/home/home.css | 84 +++++++++++++++++++++++++++++--- 2 files changed, 85 insertions(+), 7 deletions(-) diff --git a/client/src/components/layout.css b/client/src/components/layout.css index f86d4114d..3977f8413 100644 --- a/client/src/components/layout.css +++ b/client/src/components/layout.css @@ -31,3 +31,11 @@ display: flex !important; flex-direction: column !important; } + +@media (max-width: 1024px) { + .spoolman-root .ant-layout-content > div { + flex: none !important; + height: auto !important; + overflow: visible !important; + } +} diff --git a/client/src/pages/home/home.css b/client/src/pages/home/home.css index b76f2d13f..e28e57db3 100644 --- a/client/src/pages/home/home.css +++ b/client/src/pages/home/home.css @@ -72,7 +72,30 @@ @media (max-width: 600px) { .kpi-grid { - grid-template-columns: 1fr; + grid-template-columns: repeat(2, 1fr); + } +} + +/* Mobile: compact KPI cards */ +@media (max-width: 768px) { + .kpi-grid { + gap: 8px; + } + + .kpi-card { + padding: 12px 14px; + } + + .kpi-card .kpi-bg-icon { + display: none; + } + + .kpi-card .kpi-value { + font-size: 22px; + } + + .kpi-card .kpi-footer { + margin-top: 8px; } } @@ -147,12 +170,6 @@ overflow: hidden; } -@media (max-width: 1024px) { - .dashboard-main { - grid-template-columns: 1fr; - } -} - /* Left column: Tabs */ .dashboard-main .ant-tabs { display: flex; @@ -499,3 +516,56 @@ .empty-hero-btn:active { transform: scale(0.97); } + +/* ============================================ + MOBILE OVERRIDES — must be LAST to win cascade + ============================================ */ +@media (max-width: 1024px) { + .dashboard { + overflow: auto; + flex: none; + height: auto; + } + + .dashboard-main { + display: flex; + flex-direction: column; + overflow: visible; + flex: none; + height: auto; + gap: 16px; + } + + .dashboard-main .ant-tabs { + height: auto; + } + + .dashboard-main .ant-tabs-content-holder { + flex: none; + overflow: visible; + } + + .dashboard-main .ant-tabs-content, + .dashboard-main .ant-tabs-tabpane-active { + height: auto; + } + + .dashboard-main .ant-tabs-tabpane-active { + overflow-y: visible; + } + + .dashboard-main .ant-tabs-tabpane-active > .dash-section { + min-height: 0; + } + + .dash-right-col { + padding-top: 0; + height: auto; + } + + .dash-right-section { + flex: none; + height: auto; + overflow-y: visible; + } +} From a878a253e5866a14cc1d980d5b076495473141b9 Mon Sep 17 00:00:00 2001 From: RbVs Date: Tue, 24 Mar 2026 00:32:00 +0100 Subject: [PATCH 14/19] Move version & Ko-fi from sidebar footer to top bar, remove location summary - Relocate version display and Ko-fi tip button from SiderFooter to Header - Show compact (icon-only) version on mobile, full text on desktop - Remove "X Spools in Y locations" summary line from locations page - Fix React hooks order violation in LocationContainer (useState after early return) --- client/src/components/header/index.tsx | 22 +++++++++- client/src/components/layout.tsx | 42 +------------------ .../components/locationContainer.tsx | 20 +++------ 3 files changed, 27 insertions(+), 57 deletions(-) diff --git a/client/src/components/header/index.tsx b/client/src/components/header/index.tsx index 98bd8020b..00f133c80 100644 --- a/client/src/components/header/index.tsx +++ b/client/src/components/header/index.tsx @@ -1,9 +1,11 @@ import { DownOutlined } from "@ant-design/icons"; import type { RefineThemedLayoutHeaderProps } from "@refinedev/antd"; -import { useGetLocale, useSetLocale } from "@refinedev/core"; -import { Layout as AntdLayout, Button, Dropdown, MenuProps, Space, Switch, theme } from "antd"; +import { useGetLocale, useSetLocale, useTranslate } from "@refinedev/core"; +import { Grid, Layout as AntdLayout, Button, Dropdown, MenuProps, Space, Switch, theme } from "antd"; import React, { useContext } from "react"; import { ColorModeContext } from "../../contexts/color-mode"; +import { getBasePath } from "../../utils/url"; +import { Version } from "../version"; import { languages } from "../../i18n"; import QRCodeScannerModal from "../qrCodeScanner"; @@ -15,6 +17,9 @@ export const Header = ({ sticky }: RefineThemedLayoutHeaderProps) => { const locale = useGetLocale(); const changeLanguage = useSetLocale(); const { mode, setMode } = useContext(ColorModeContext); + const t = useTranslate(); + const screens = Grid.useBreakpoint(); + const isMobile = !screens.md; const currentLocale = locale(); @@ -41,6 +46,19 @@ export const Header = ({ sticky }: RefineThemedLayoutHeaderProps) => { return ( + + {isMobile ? : {t("version")} } + + { - const t = useTranslate(); - - if (collapsed) { - return ( -
    -
    - ); - } - - return ( -
    -
    - {t("version")} -
    - -
    - ); -}; import "./layout.css"; @@ -75,7 +36,6 @@ export const SpoolmanLayout = ({ children }: { children: React.ReactNode }) => ( {bottomItems} {logout} - ); }} diff --git a/client/src/pages/locations/components/locationContainer.tsx b/client/src/pages/locations/components/locationContainer.tsx index d007ac39a..0abbd8d4a 100644 --- a/client/src/pages/locations/components/locationContainer.tsx +++ b/client/src/pages/locations/components/locationContainer.tsx @@ -150,6 +150,10 @@ export function LocationContainer() { } }, [locationsList, settingsLocations, setLocationsSetting]); + const [modalOpen, setModalOpen] = useState(false); + const [newLocationName, setNewLocationName] = useState(""); + const [modalError, setModalError] = useState(""); + if (isLoading) { return (
    @@ -162,10 +166,6 @@ export function LocationContainer() { return
    Failed to load spools
    ; } - const [modalOpen, setModalOpen] = useState(false); - const [newLocationName, setNewLocationName] = useState(""); - const [modalError, setModalError] = useState(""); - const addNewLocation = () => { const name = newLocationName.trim(); if (!name) { @@ -185,10 +185,6 @@ export function LocationContainer() { setModalError(""); }; - // Count totals - const totalSpools = spoolData?.data?.length ?? 0; - const totalLocations = locationsList.filter((l) => l !== EMPTYLOC).length; - return (
    {modalError &&
    {modalError}
    }
    -
    - - {totalSpools} {t("spool.spool", { count: totalSpools })} in {totalLocations}{" "} - {t("locations.locations").toLowerCase()} - +
    - {!isLoading && totalSpools == 0 && ( + {!isLoading && (spoolData?.data?.length ?? 0) === 0 && (
    {t("locations.no_locations_help")}
    From c3a5974bccaafa409464f064a317a954f078bec9 Mon Sep 17 00:00:00 2001 From: RbVs Date: Tue, 24 Mar 2026 00:48:14 +0100 Subject: [PATCH 15/19] Mobile icon-only list header buttons, move locations button to List header - Add CSS to hide button text on mobile (<=768px), showing only icons - Move "New Location" button from LocationContainer into List headerButtons to match spool/filament/vendor pattern and enable mobile icon-only styling --- client/src/components/layout.css | 15 +++++++++++++++ .../locations/components/locationContainer.tsx | 16 +++++++--------- client/src/pages/locations/index.tsx | 17 +++++++++++++++-- 3 files changed, 37 insertions(+), 11 deletions(-) diff --git a/client/src/components/layout.css b/client/src/components/layout.css index 3977f8413..f1a806e5a 100644 --- a/client/src/components/layout.css +++ b/client/src/components/layout.css @@ -39,3 +39,18 @@ overflow: visible !important; } } + +/* Mobile: show only icons in list header buttons */ +@media (max-width: 768px) { + [class*="page-header-heading-extra"] .ant-btn > .ant-btn-icon + span { + display: none !important; + } + + [class*="page-header-heading-extra"] .ant-space { + gap: 4px !important; + } + + [class*="page-header-heading-extra"] .ant-btn { + padding-inline: 8px !important; + } +} diff --git a/client/src/pages/locations/components/locationContainer.tsx b/client/src/pages/locations/components/locationContainer.tsx index 0abbd8d4a..26ce98f98 100644 --- a/client/src/pages/locations/components/locationContainer.tsx +++ b/client/src/pages/locations/components/locationContainer.tsx @@ -1,13 +1,17 @@ -import { PlusOutlined } from "@ant-design/icons"; import { useList, useTranslate } from "@refinedev/core"; -import { Button, Input, Modal, Spin } from "antd"; +import { Input, Modal, Spin } from "antd"; import { useEffect, useMemo, useState } from "react"; import { useSetSetting } from "../../../utils/querySettings"; import { ISpool } from "../../spools/model"; import { EMPTYLOC, useLocations, useLocationsSpoolOrders, useRenameSpoolLocation } from "../functions"; import { Location } from "./location"; -export function LocationContainer() { +interface LocationContainerProps { + modalOpen: boolean; + setModalOpen: (open: boolean) => void; +} + +export function LocationContainer({ modalOpen, setModalOpen }: LocationContainerProps) { const t = useTranslate(); const renameSpoolLocation = useRenameSpoolLocation(); @@ -150,7 +154,6 @@ export function LocationContainer() { } }, [locationsList, settingsLocations, setLocationsSetting]); - const [modalOpen, setModalOpen] = useState(false); const [newLocationName, setNewLocationName] = useState(""); const [modalError, setModalError] = useState(""); @@ -213,11 +216,6 @@ export function LocationContainer() { /> {modalError &&
    {modalError}
    } -
    - -
    {!isLoading && (spoolData?.data?.length ?? 0) === 0 && (
    {t("locations.no_locations_help")} diff --git a/client/src/pages/locations/index.tsx b/client/src/pages/locations/index.tsx index 1c7e5f880..27dfa2f31 100644 --- a/client/src/pages/locations/index.tsx +++ b/client/src/pages/locations/index.tsx @@ -1,4 +1,8 @@ +import { PlusOutlined } from "@ant-design/icons"; import { List } from "@refinedev/antd"; +import { useTranslate } from "@refinedev/core"; +import { Button } from "antd"; +import { useState } from "react"; import dayjs from "dayjs"; import utc from "dayjs/plugin/utc"; import { DndProvider } from "react-dnd"; @@ -10,10 +14,19 @@ import "./locations.css"; dayjs.extend(utc); export const Locations = () => { + const t = useTranslate(); + const [modalOpen, setModalOpen] = useState(false); + return ( - + ( + + )} + > - + ); From b02351af0b0a67002b3d130673d61719d4f0c0cc Mon Sep 17 00:00:00 2001 From: RbVs Date: Tue, 24 Mar 2026 00:52:46 +0100 Subject: [PATCH 16/19] Fix mobile layout: move hamburger into header row, offset version/kofi - Override sider hamburger top position from 64px to 12px on mobile - Add marginLeft on mobile to prevent version/kofi overlapping hamburger --- client/src/components/header/index.tsx | 2 +- client/src/components/layout.css | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/client/src/components/header/index.tsx b/client/src/components/header/index.tsx index 00f133c80..686f48e38 100644 --- a/client/src/components/header/index.tsx +++ b/client/src/components/header/index.tsx @@ -46,7 +46,7 @@ export const Header = ({ sticky }: RefineThemedLayoutHeaderProps) => { return ( - + {isMobile ? : {t("version")} }