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:
1 parent
f16c8c73c6
commit
b30acb652a
20 files changed
+1182
-82
No files matched your search
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in new issue
Block a user