Add "maybe" status option and long-press functionality
This update introduces a new "maybe" status for entries, represented by a distinct UI button and background. Additionally, long-press functionality has been implemented, allowing users to toggle the "confirmed" status through prolonged interaction. UI components and styles have been updated to reflect these changes, improving usability.
This commit is contained in:
@@ -75,7 +75,7 @@ def patch_sheet(req: Request, game_id: str, entry_id: str, data: dict, db: Sessi
|
|||||||
|
|
||||||
if note_tag not in (None, "i", "m", "s"):
|
if note_tag not in (None, "i", "m", "s"):
|
||||||
raise HTTPException(400, "invalid note_tag")
|
raise HTTPException(400, "invalid note_tag")
|
||||||
if status is not None and status not in (0, 1, 2):
|
if status is not None and status not in (0, 1, 2, 3):
|
||||||
raise HTTPException(400, "invalid status")
|
raise HTTPException(400, "invalid status")
|
||||||
|
|
||||||
st = db.query(SheetState).filter(
|
st = db.query(SheetState).filter(
|
||||||
|
|||||||
@@ -62,7 +62,6 @@ function AdminPanel() {
|
|||||||
const closeModal = () => {
|
const closeModal = () => {
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
setMsg("");
|
setMsg("");
|
||||||
// optional: resetForm(); // wenn du beim Schließen leeren willst
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -87,7 +86,6 @@ function AdminPanel() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Modal */}
|
|
||||||
{open && (
|
{open && (
|
||||||
<div style={styles.modalOverlay} onMouseDown={closeModal}>
|
<div style={styles.modalOverlay} onMouseDown={closeModal}>
|
||||||
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
<div style={styles.modalCard} onMouseDown={(e) => e.stopPropagation()}>
|
||||||
@@ -128,7 +126,7 @@ function AdminPanel() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ fontSize: 12, opacity: 0.75 }}>
|
<div style={{ fontSize: 12, opacity: 0.75 }}>
|
||||||
Tipp: Am Handy ist das Modal leichter zu bedienen als die Grid-Zeile.
|
Tipp: Tap auf Item = rot (ausschließen), Long-Press = grün (bestätigt), ? = unsicher.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -138,6 +136,38 @@ function AdminPanel() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function useLongPress(onLongPress, onClick, { delay = 450 } = {}) {
|
||||||
|
const [longPressed, setLongPressed] = useState(false);
|
||||||
|
const timeoutRef = React.useRef(null);
|
||||||
|
|
||||||
|
const start = (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
setLongPressed(false);
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
setLongPressed(true);
|
||||||
|
onLongPress();
|
||||||
|
}, delay);
|
||||||
|
};
|
||||||
|
|
||||||
|
const clear = () => {
|
||||||
|
if (timeoutRef.current) clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const end = () => {
|
||||||
|
clear();
|
||||||
|
if (!longPressed) onClick();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
onMouseDown: start,
|
||||||
|
onMouseUp: end,
|
||||||
|
onMouseLeave: clear,
|
||||||
|
onTouchStart: start,
|
||||||
|
onTouchEnd: end,
|
||||||
|
onTouchMove: clear,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [me, setMe] = useState(null);
|
const [me, setMe] = useState(null);
|
||||||
@@ -154,17 +184,12 @@ export default function App() {
|
|||||||
const gs = await api("/games");
|
const gs = await api("/games");
|
||||||
setGames(gs);
|
setGames(gs);
|
||||||
|
|
||||||
// wenn noch kein game ausgewählt ist -> erstes nehmen
|
|
||||||
if (gs[0] && !gameId) setGameId(gs[0].id);
|
if (gs[0] && !gameId) setGameId(gs[0].id);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try { await load(); } catch {}
|
||||||
await load();
|
|
||||||
} catch {
|
|
||||||
// not logged in
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
@@ -175,9 +200,7 @@ export default function App() {
|
|||||||
try {
|
try {
|
||||||
const sh = await api(`/games/${gameId}/sheet`);
|
const sh = await api(`/games/${gameId}/sheet`);
|
||||||
setSheet(sh);
|
setSheet(sh);
|
||||||
} catch {
|
} catch {}
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
})();
|
})();
|
||||||
}, [gameId]);
|
}, [gameId]);
|
||||||
|
|
||||||
@@ -222,6 +245,24 @@ export default function App() {
|
|||||||
await reloadSheet();
|
await reloadSheet();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const toggleConfirmed = async (entry) => {
|
||||||
|
const next = entry.status === 2 ? 0 : 2;
|
||||||
|
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ status: next }),
|
||||||
|
});
|
||||||
|
await reloadSheet();
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleMaybe = async (entry) => {
|
||||||
|
const next = entry.status === 3 ? 0 : 3;
|
||||||
|
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
||||||
|
method: "PATCH",
|
||||||
|
body: JSON.stringify({ status: next }),
|
||||||
|
});
|
||||||
|
await reloadSheet();
|
||||||
|
};
|
||||||
|
|
||||||
const toggleTag = async (entry) => {
|
const toggleTag = async (entry) => {
|
||||||
const next = cycleTag(entry.note_tag);
|
const next = cycleTag(entry.note_tag);
|
||||||
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
await api(`/games/${gameId}/sheet/${entry.entry_id}`, {
|
||||||
@@ -231,7 +272,6 @@ export default function App() {
|
|||||||
await reloadSheet();
|
await reloadSheet();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Login Screen
|
|
||||||
if (!me) {
|
if (!me) {
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
@@ -275,10 +315,23 @@ export default function App() {
|
|||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
|
|
||||||
|
const getRowBg = (status) => {
|
||||||
|
if (status === 1) return "rgba(255, 0, 0, 0.06)"; // crossed
|
||||||
|
if (status === 2) return "rgba(0, 170, 60, 0.07)"; // confirmed
|
||||||
|
if (status === 3) return "rgba(255, 180, 70, 0.12)"; // maybe
|
||||||
|
return "rgba(255,255,255,0.22)";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getNameColor = (status) => {
|
||||||
|
if (status === 1) return "#b10000";
|
||||||
|
if (status === 2) return "#0b6a1e";
|
||||||
|
if (status === 3) return "#6b4d00";
|
||||||
|
return "#20140c";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={styles.page}>
|
<div style={styles.page}>
|
||||||
<div style={styles.shell}>
|
<div style={styles.shell}>
|
||||||
{/* Top Bar */}
|
|
||||||
<div style={styles.topBar}>
|
<div style={styles.topBar}>
|
||||||
<div>
|
<div>
|
||||||
<div style={{ fontWeight: 900, color: "#20140c" }}>{me.email}</div>
|
<div style={{ fontWeight: 900, color: "#20140c" }}>{me.email}</div>
|
||||||
@@ -291,10 +344,8 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Admin Panel */}
|
|
||||||
{me.role === "admin" && <AdminPanel />}
|
{me.role === "admin" && <AdminPanel />}
|
||||||
|
|
||||||
{/* Game Selector */}
|
|
||||||
<div style={{ marginTop: 14 }}>
|
<div style={{ marginTop: 14 }}>
|
||||||
<div style={styles.card}>
|
<div style={styles.card}>
|
||||||
<div style={styles.sectionHeader}>Spiel</div>
|
<div style={styles.sectionHeader}>Spiel</div>
|
||||||
@@ -314,56 +365,65 @@ export default function App() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Sheet */}
|
|
||||||
<div style={{ marginTop: 14, display: "grid", gap: 14 }}>
|
<div style={{ marginTop: 14, display: "grid", gap: 14 }}>
|
||||||
{sections.map((sec) => (
|
{sections.map((sec) => (
|
||||||
<div key={sec.key} style={styles.card}>
|
<div key={sec.key} style={styles.card}>
|
||||||
<div style={styles.sectionHeader}>{sec.title}</div>
|
<div style={styles.sectionHeader}>{sec.title}</div>
|
||||||
|
|
||||||
<div style={{ display: "grid" }}>
|
<div style={{ display: "grid" }}>
|
||||||
{sec.entries.map((e) => (
|
{sec.entries.map((e) => {
|
||||||
|
const pressHandlers = useLongPress(
|
||||||
|
() => toggleConfirmed(e), // long press -> confirmed
|
||||||
|
() => toggleCross(e), // tap -> crossed
|
||||||
|
{ delay: 450 }
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
<div
|
<div
|
||||||
key={e.entry_id}
|
key={e.entry_id}
|
||||||
style={{
|
style={{
|
||||||
...styles.row,
|
...styles.row,
|
||||||
background:
|
background: getRowBg(e.status),
|
||||||
e.status === 1
|
|
||||||
? "rgba(255, 0, 0, 0.06)" // leicht rot
|
|
||||||
: "rgba(255,255,255,0.22)",
|
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{/* Name: Tap toggelt crossed */}
|
{/* Name: Tap = rot, Long-Press = grün */}
|
||||||
<div
|
<div
|
||||||
onClick={() => toggleCross(e)}
|
{...pressHandlers}
|
||||||
style={{
|
style={{
|
||||||
...styles.name,
|
...styles.name,
|
||||||
textDecoration: e.status === 1 ? "line-through" : "none",
|
textDecoration: e.status === 1 ? "line-through" : "none",
|
||||||
|
color: getNameColor(e.status),
|
||||||
// ✅ Rot wenn gestrichen
|
|
||||||
color: e.status === 1 ? "#b10000" : "#20140c",
|
|
||||||
|
|
||||||
// optional etwas transparenter
|
|
||||||
opacity: e.status === 1 ? 0.75 : 1,
|
opacity: e.status === 1 ? 0.75 : 1,
|
||||||
}}
|
}}
|
||||||
|
title="Tippen = ausschließen | Lange halten = bestätigt"
|
||||||
>
|
>
|
||||||
{e.label}
|
{e.label}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Spalte 1: X */}
|
{/* ? = maybe */}
|
||||||
|
<button
|
||||||
|
onClick={() => toggleMaybe(e)}
|
||||||
|
style={{
|
||||||
|
...styles.maybeBtn,
|
||||||
|
background: e.status === 3
|
||||||
|
? "linear-gradient(180deg, rgba(255,210,120,0.9), rgba(180,120,20,0.25))"
|
||||||
|
: styles.maybeBtn.background,
|
||||||
|
}}
|
||||||
|
title="Unsicher"
|
||||||
|
>
|
||||||
|
?
|
||||||
|
</button>
|
||||||
|
|
||||||
<div style={styles.cell}>{e.status === 1 ? "X" : ""}</div>
|
<div style={styles.cell}>{e.status === 1 ? "X" : ""}</div>
|
||||||
|
|
||||||
{/* Spalte 2: gelbes ✓ */}
|
|
||||||
<div style={styles.cell}>{e.status === 1 ? "✓" : ""}</div>
|
<div style={styles.cell}>{e.status === 1 ? "✓" : ""}</div>
|
||||||
|
|
||||||
{/* Spalte 3: confirmed (grünes ✓) – aktuell nicht per UI gesetzt */}
|
|
||||||
<div style={styles.cell}>{e.status === 2 ? "✓" : ""}</div>
|
<div style={styles.cell}>{e.status === 2 ? "✓" : ""}</div>
|
||||||
|
|
||||||
{/* Spalte 4: i/m/s */}
|
|
||||||
<button onClick={() => toggleTag(e)} style={styles.tagBtn} title="i → m → s → leer">
|
<button onClick={() => toggleTag(e)} style={styles.tagBtn} title="i → m → s → leer">
|
||||||
{e.note_tag || "—"}
|
{e.note_tag || "—"}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
))}
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
@@ -426,7 +486,7 @@ const styles = {
|
|||||||
},
|
},
|
||||||
row: {
|
row: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 40px 40px 40px 56px",
|
gridTemplateColumns: "1fr 42px 40px 40px 40px 56px", // ✅ extra Spalte für ?
|
||||||
gap: 8,
|
gap: 8,
|
||||||
padding: "10px 12px",
|
padding: "10px 12px",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
@@ -444,12 +504,22 @@ const styles = {
|
|||||||
fontWeight: 1000,
|
fontWeight: 1000,
|
||||||
color: "#20140c",
|
color: "#20140c",
|
||||||
},
|
},
|
||||||
|
maybeBtn: {
|
||||||
|
padding: "8px 0",
|
||||||
|
fontWeight: 1000,
|
||||||
|
borderRadius: 10,
|
||||||
|
border: "1px solid rgba(0,0,0,0.25)",
|
||||||
|
background: "linear-gradient(180deg, rgba(255,255,255,0.55), rgba(0,0,0,0.06))",
|
||||||
|
color: "#6b4d00",
|
||||||
|
cursor: "pointer",
|
||||||
|
},
|
||||||
tagBtn: {
|
tagBtn: {
|
||||||
padding: "8px 0",
|
padding: "8px 0",
|
||||||
fontWeight: 1000,
|
fontWeight: 1000,
|
||||||
borderRadius: 10,
|
borderRadius: 10,
|
||||||
border: "1px solid rgba(0,0,0,0.25)",
|
border: "1px solid rgba(0,0,0,0.25)",
|
||||||
background: "linear-gradient(180deg, rgba(255,255,255,0.55), rgba(0,0,0,0.06))",
|
background: "linear-gradient(180deg, rgba(255,255,255,0.55), rgba(0,0,0,0.06))",
|
||||||
|
cursor: "pointer",
|
||||||
},
|
},
|
||||||
input: {
|
input: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
@@ -488,11 +558,6 @@ const styles = {
|
|||||||
color: "#20140c",
|
color: "#20140c",
|
||||||
marginBottom: 8,
|
marginBottom: 8,
|
||||||
},
|
},
|
||||||
adminGrid: {
|
|
||||||
display: "grid",
|
|
||||||
gridTemplateColumns: "1fr 1fr 120px 140px",
|
|
||||||
gap: 8,
|
|
||||||
},
|
|
||||||
userRow: {
|
userRow: {
|
||||||
display: "grid",
|
display: "grid",
|
||||||
gridTemplateColumns: "1fr 80px 90px",
|
gridTemplateColumns: "1fr 80px 90px",
|
||||||
|
|||||||
Reference in New Issue
Block a user