Listen mit eigenem Vorrat und Einkauf, Städte anderer auf der Reise-Karte, Design "Fabrik"

Vorrat & Einkauf:
- Mehrere Listen (z. B. "Zuhause", "Urlaub"), jede mit eigenem Vorrat und
  eigener Einkaufsliste; Auswahl oben auf der Seite, gemerkt pro Browser
- Beim Anlegen festlegen, wer die Liste sieht: alle Konten oder nur
  ausgewählte (Ersteller sieht sie immer); ändern/löschen nur Ersteller
  und Administratoren
- Gekauft, aufgebraucht, Nachkaufen und Rezeptabgleich wirken innerhalb
  der gewählten Liste
- Migration: bisheriger Vorrat und Einkauf werden zur Liste "Zuhause"
  (für alle sichtbar)

Reisen:
- Schalter "Städte anderer zeigen": Städte der übrigen Konten in je einer
  eigenen Farbe, einzelne Personen ein-/ausblendbar, nur lesend
- Info-Karte zeigt, wer noch in einer Stadt bzw. einem Land war

Design "Fabrik" (Industrie-Inventar à la Satisfactory):
- Stahlplatten mit Schrauben, Titelleisten mit Fenstersymbol, gelb
  leuchtende Hauptbuttons, orange Mengen-Zähler, Warnstreifen,
  Riffelblech und drehende Zahnräder im Hintergrund
- Schriften Roboto / Roboto Condensed (OFL), lokal eingebunden

Doku: README verlinkt das zugehörige Repository P42-S.M.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
guyg1474andClaude Opus 5.5 committed 2026-10-15 11:50:46 +02:00
1 parent f16c8c73c6
commit b30acb652a
20 files changed
+1182 -82

No files matched your search

+28
View File
@@ -0,0 +1,28 @@
// Hintergrund "Fabrik" (Industrie-UI im Stil von Satisfactory): Hallenwand mit Riffelblech-Boden,
// HUD-Linie oben und zwei langsam drehende Zahnräder. Nur CSS/SVG, ohne Animation steht alles still.
// Zahnrad als Umriss mit Loch (evenodd): Zähne trapezförmig zwischen Fuß- und Kopfkreis
function gearPath(teeth, outer, inner, hole) {
const step = (Math.PI * 2) / teeth;
const pts = [];
for (let i = 0; i < teeth; i += 1) {
const a = i * step;
pts.push([inner, a - step * 0.32], [outer, a - step * 0.18], [outer, a + step * 0.18], [inner, a + step * 0.32]);
}
const ring = pts.map(([r, a], i) => `${i ? 'L' : 'M'}${(r * Math.cos(a)).toFixed(2)} ${(r * Math.sin(a)).toFixed(2)}`).join('');
return `${ring}Z M${hole} 0A${hole} ${hole} 0 1 0 ${-hole} 0A${hole} ${hole} 0 1 0 ${hole} 0Z`;
}
const BIG = gearPath(14, 48, 40, 15);
const SMALL = gearPath(10, 48, 38, 18);
export default function FabrikField() {
return (
<div className="theme-bg fabrik-field" aria-hidden="true">
<div className="fabrik-floor" />
<svg className="fabrik-gear big" viewBox="-50 -50 100 100"><path d={BIG} fillRule="evenodd" /></svg>
<svg className="fabrik-gear small" viewBox="-50 -50 100 100"><path d={SMALL} fillRule="evenodd" /></svg>
<div className="fabrik-hud" />
</div>
);
}
+4 -4
View File
@@ -21,9 +21,9 @@ function loadPeople() {
/**
* Rezept abgleichen: Zutaten laden, auf Personenzahl umrechnen, mit dem Vorrat vergleichen
* und Fehlendes auf die Einkaufsliste setzen. find(list, name) sucht in den aktuellen Listen.
* und Fehlendes in den Einkauf der gewählten Liste setzen. find(list, name) sucht in Vorrat bzw. Einkauf dieser Liste.
*/
export default function RecipeCheck({ products, find, onTakeOver, busy }) {
export default function RecipeCheck({ products, find, shoppingList, onTakeOver, busy }) {
const [mode, setMode] = useState('url'); // url | text
const [url, setUrl] = useState('');
const [text, setText] = useState('');
@@ -266,10 +266,10 @@ export default function RecipeCheck({ products, find, onTakeOver, busy }) {
{recipe.invalid
? `Bitte die Menge bei „${recipe.invalid}“ prüfen (Anzahl: ganze Zahl, kg/l: z. B. 0,5).`
: recipe.done != null
? `✓ ${recipe.done} ${recipe.done === 1 ? 'Zutat' : 'Zutaten'} auf die Einkaufsliste gesetzt.`
? `✓ ${recipe.done} ${recipe.done === 1 ? 'Zutat' : 'Zutaten'} auf die Einkaufsliste „${shoppingList?.name}“ gesetzt.`
: 'Gramm und ml werden mit kg/l im Vorrat verglichen. EL, Packungen usw. nur auf „vorhanden“.'}
</p>
<button className="btn" type="button" disabled={busy || chosen.length === 0} onClick={takeOver}>
<button className="btn" type="button" disabled={busy || chosen.length === 0 || !shoppingList} onClick={takeOver}>
Fehlendes auf Einkaufsliste ({chosen.length})
</button>
</footer>
@@ -1,12 +1,14 @@
import { useEffect, useState } from 'react';
import { getTheme, onThemeChange } from '../theme.js';
import BlackwallField from './BlackwallField.jsx';
import FabrikField from './FabrikField.jsx';
import MatrixRain from './MatrixRain.jsx';
// Dekorativer Hintergrund des aktiven Designs (alles aria-hidden, ohne Mausereignisse)
const BACKGROUNDS = {
matrix: MatrixRain,
blackwall: BlackwallField,
fabrik: FabrikField,
};
export default function ThemeBackground() {
+6 -4
View File
@@ -28,10 +28,11 @@ function mainPart(feature) {
/**
* Weltkarte mit Ländern und Stadt-Punkten. Zoomen/Verschieben per Maus, Touch und Buttons.
* countries: [{ key, name, feature }], visited: Set der Länder-Keys, selected: { type, key }.
* otherCities: Städte anderer Konten ({ ...city, owner: { name }, color: 1–6 }), unter den eigenen gezeichnet.
* Über `ref`: focusCountry(key, offset), focusCity(city, offset), zoomBy(f), reset().
* offset: Ziel nach rechts oben schieben, weil unten links der Info-Kasten liegt.
*/
export default function WorldMap({ ref, countries, visited, cities, selected, onSelect }) {
export default function WorldMap({ ref, countries, visited, cities, otherCities = [], selected, onSelect }) {
const box = useRef(null);
const svgRef = useRef(null);
const layer = useRef(null);
@@ -132,17 +133,18 @@ export default function WorldMap({ ref, countries, visited, cities, selected, on
<title>{c.name}</title>
</path>
))}
{cities.map((city) => {
{[...otherCities, ...cities].map((city) => {
const p = projection([city.lon, city.lat]);
if (!p) return null;
const isSelected = selected?.type === 'city' && selected.key === city.id;
return (
<g key={city.id} transform={`translate(${p[0]},${p[1]})`}>
<g
className={`map-city${isSelected ? ' selected' : ''}`}
className={`map-city${city.owner ? ' other' : ''}${isSelected ? ' selected' : ''}`}
style={city.owner ? { '--dot': `var(--other-${city.color})` } : undefined}
onClick={(e) => { e.stopPropagation(); onSelect({ type: 'city', key: city.id }); }}
>
<title>{city.name}</title>
<title>{city.owner ? `${city.name} (${city.owner.name})` : city.name}</title>
<circle className="map-city-hit" r="11" />
{isSelected && <circle className="map-city-ring" r="9" />}
<circle className="map-city-dot" r="4" />