Skip to content

Commit 882b60c

Browse files
author
cwlowder
committed
Implement currency setting toggle
1 parent 0f3d97f commit 882b60c

13 files changed

Lines changed: 235 additions & 34 deletions

File tree

backend/app/db.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,7 +74,8 @@
7474
# {"type": "regular", "minutes": 15, "small_blind_cents": 300, "big_blind_cents": 600, "ante_cents": 0}
7575
],
7676
"sounds": {"transition": None, "half": None, "thirty": None, "five": None, "end": None},
77-
"seating": {"min_players_per_table": 4}
77+
"seating": {"min_players_per_table": 4},
78+
"currency": {"symbol": "$", "denomination": "cents"}
7879
}
7980

8081
DEFAULT_STATE = {

frontend/src/components/MoneyDisplay.tsx

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import React, { useState, useEffect } from "react";
2-
import { centsToMoney } from "../utils/time";
2+
import { centsToMoney, centsToWhole } from "../utils/money";
3+
import { Denomination } from "../types";
34

45
export default function MoneyDisplay({
56
cents,
@@ -8,6 +9,8 @@ export default function MoneyDisplay({
89
editable = false,
910
disabled = false,
1011
increment = 0.1,
12+
currencySymbol = "$",
13+
denomination = "cents",
1114
onChange
1215
}: {
1316
cents: number;
@@ -16,23 +19,31 @@ export default function MoneyDisplay({
1619
editable?: boolean;
1720
disabled?: boolean;
1821
increment?: number;
22+
currencySymbol?: string;
23+
denomination?: Denomination;
1924
onChange?: (cents: number) => void;
2025
}) {
21-
const { dollars, cents: cc } = centsToMoney(cents);
26+
const isWhole = denomination === "whole";
2227
const isEditableDisabled = editable && disabled;
2328

24-
const [draft, setDraft] = useState((cents / 100).toFixed(2));
29+
// For "whole" mode the stored value IS the display value (no /100 conversion).
30+
// For "cents" mode the stored value is cents, display is dollars.cents.
31+
const toDisplay = (c: number) => isWhole ? String(c) : (c / 100).toFixed(2);
32+
const fromDisplay = (v: number) => isWhole ? Math.round(v) : Math.round(v * 100);
33+
const editStep = isWhole ? 1 : increment;
34+
35+
const [draft, setDraft] = useState(toDisplay(cents));
2536

2637
useEffect(() => {
27-
setDraft((cents / 100).toFixed(2));
28-
}, [cents]);
38+
setDraft(toDisplay(cents));
39+
}, [cents, denomination]);
2940

3041
if (editable) {
3142
return (
3243
<input
3344
className="input"
3445
type="number"
35-
step={increment.toFixed(2)}
46+
step={isWhole ? String(editStep) : editStep.toFixed(2)}
3647
min="0"
3748
value={draft}
3849
disabled={disabled}
@@ -50,16 +61,27 @@ export default function MoneyDisplay({
5061

5162
const parsed = Number(value);
5263
if (!isNaN(parsed) && onChange) {
53-
onChange(Math.round(parsed * 100));
64+
onChange(fromDisplay(parsed));
5465
}
5566
}}
5667
/>
5768
);
5869
}
5970

71+
if (isWhole) {
72+
const display = centsToWhole(cents);
73+
return (
74+
<span style={{ fontSize: size, fontWeight: 800, opacity: muted ? 0.8 : 1 }}>
75+
{currencySymbol}{display}
76+
</span>
77+
);
78+
}
79+
80+
const { dollars, cents: cc } = centsToMoney(cents);
81+
6082
return (
6183
<span style={{ fontSize: size, fontWeight: 800, opacity: muted ? 0.8 : 1 }}>
62-
${dollars}
84+
{currencySymbol}{dollars}
6385
<span
6486
style={{
6587
fontSize: Math.max(12, Math.round(size * 0.62)),

frontend/src/components/TimerCard.tsx

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import React from "react";
22
import MoneyDisplay from "./MoneyDisplay";
33
import { msToClock } from "../utils/time";
4-
import { Level, State } from "../types";
4+
import { Denomination, Level, State } from "../types";
55
import {
66
CupSoda,
77
PiggyBank,
@@ -33,12 +33,16 @@ export default function TimerCard({
3333
state,
3434
levels,
3535
remainingMs,
36-
bigPic = false
36+
bigPic = false,
37+
currencySymbol = "$",
38+
denomination = "cents"
3739
}: {
3840
state: State;
3941
levels: Level[];
4042
remainingMs: number;
4143
bigPic?: boolean;
44+
currencySymbol?: string;
45+
denomination?: Denomination;
4246
}) {
4347
const { t } = useTranslation();
4448

@@ -131,13 +135,13 @@ export default function TimerCard({
131135
<div className={`${blindClass}`}>{t("timer.breakText")}</div>
132136
) : (
133137
<div className={`${blindClass}`}>
134-
<MoneyDisplay cents={cur?.small_blind_cents ?? 0} size={blindSize} />
138+
<MoneyDisplay cents={cur?.small_blind_cents ?? 0} size={blindSize} currencySymbol={currencySymbol} denomination={denomination} />
135139
<span className="muted"> / </span>
136-
<MoneyDisplay cents={cur?.big_blind_cents ?? 0} size={blindSize} />
140+
<MoneyDisplay cents={cur?.big_blind_cents ?? 0} size={blindSize} currencySymbol={currencySymbol} denomination={denomination} />
137141
{cur?.ante_cents ? (
138142
<>
139143
<span className="muted">{t("common.ante")} </span>
140-
<MoneyDisplay cents={cur.ante_cents} size={blindSize} muted />
144+
<MoneyDisplay cents={cur.ante_cents} size={blindSize} muted currencySymbol={currencySymbol} denomination={denomination} />
141145
</>
142146
) : null}
143147
</div>
@@ -154,9 +158,9 @@ export default function TimerCard({
154158
<div className={`muted ${blindClass}`}>{t("timer.breakNextMin", { min : next.minutes })}</div>
155159
) : (
156160
<div className={`muted ${blindClass}`}>
157-
<MoneyDisplay cents={next.small_blind_cents} size={blindSize} muted />
161+
<MoneyDisplay cents={next.small_blind_cents} size={blindSize} muted currencySymbol={currencySymbol} denomination={denomination} />
158162
<span className="muted"> / </span>
159-
<MoneyDisplay cents={next.big_blind_cents} size={blindSize} muted />
163+
<MoneyDisplay cents={next.big_blind_cents} size={blindSize} muted currencySymbol={currencySymbol} denomination={denomination} />
160164
<span className="muted">{t("timer.nextMin", { min: next.minutes })}</span>
161165
</div>
162166
)

frontend/src/components/admin/levelsTab.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,22 +3,26 @@ import { GripVertical, MoveUp, MoveDown, X } from "lucide-react";
33
import { useTranslation } from "react-i18next";
44

55
import MoneyDisplay from "../MoneyDisplay";
6-
import { Level, Settings } from "../../types";
6+
import { Denomination, Level, Settings } from "../../types";
77

88
function LevelsCard({
99
settings,
1010
levelsDraft,
1111
levelsDirty,
1212
setLevelsDirty,
1313
setLevelsDraft,
14-
onSave
14+
onSave,
15+
currencySymbol = "$",
16+
denomination = "cents"
1517
}: {
1618
settings: Settings | null;
1719
levelsDraft: Level[];
1820
levelsDirty: boolean;
1921
setLevelsDirty: (b: boolean) => void;
2022
setLevelsDraft: React.Dispatch<React.SetStateAction<Level[]>>;
2123
onSave: () => Promise<void>;
24+
currencySymbol?: string;
25+
denomination?: Denomination;
2226
}) {
2327
const { t } = useTranslation();
2428

@@ -278,6 +282,8 @@ function LevelsCard({
278282
size={20}
279283
editable
280284
disabled={isBreak}
285+
currencySymbol={currencySymbol}
286+
denomination={denomination}
281287
onChange={(cents) => updateLevel(idx, { small_blind_cents: cents })}
282288
/>
283289
</td>
@@ -288,6 +294,8 @@ function LevelsCard({
288294
size={20}
289295
editable
290296
disabled={isBreak}
297+
currencySymbol={currencySymbol}
298+
denomination={denomination}
291299
onChange={(cents) => updateLevel(idx, { big_blind_cents: cents })}
292300
/>
293301
</td>
@@ -298,6 +306,8 @@ function LevelsCard({
298306
size={20}
299307
editable
300308
disabled={isBreak}
309+
currencySymbol={currencySymbol}
310+
denomination={denomination}
301311
onChange={(cents) => updateLevel(idx, { ante_cents: cents })}
302312
/>
303313
</td>
@@ -353,14 +363,18 @@ export function LevelsTab({
353363
levelsDirty,
354364
setLevelsDraft,
355365
setLevelsDirty,
356-
onSaveLevels
366+
onSaveLevels,
367+
currencySymbol = "$",
368+
denomination = "cents"
357369
}: {
358370
settings: Settings | null;
359371
levelsDraft: Level[];
360372
levelsDirty: boolean;
361373
setLevelsDraft: React.Dispatch<React.SetStateAction<Level[]>>;
362374
setLevelsDirty: (b: boolean) => void;
363375
onSaveLevels: () => Promise<void>;
376+
currencySymbol?: string;
377+
denomination?: Denomination;
364378
}) {
365379
return (
366380
<div style={{ marginTop: 12 }}>
@@ -371,6 +385,8 @@ export function LevelsTab({
371385
setLevelsDraft={setLevelsDraft}
372386
setLevelsDirty={setLevelsDirty}
373387
onSave={onSaveLevels}
388+
currencySymbol={currencySymbol}
389+
denomination={denomination}
374390
/>
375391
</div>
376392
);

frontend/src/components/admin/settingsTab.tsx

Lines changed: 117 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import { Settings } from "../../types";
1+
import { Denomination, Settings } from "../../types";
22
import React, { useState } from "react";
33
import { useTranslation } from "react-i18next";
44
import { noVolume, halfVolume, fullVolume } from "../../hooks/useLocalSettings";
55
import { useLocalSettingsCtx } from "../../context/LocalSettingsContext";
6+
import MoneyDisplay from "../MoneyDisplay";
67

78
export function SeatingCard({
89
settings,
@@ -155,22 +156,136 @@ export function SoundsCard({
155156
}
156157

157158

159+
const CURRENCY_OPTIONS = [
160+
{ label: "$ (USD)", value: "$" },
161+
{ label: "\u00a3 (GBP)", value: "\u00a3" },
162+
{ label: "\u20ac (EUR)", value: "\u20ac" },
163+
{ label: "\u00a5 (JPY)", value: "\u00a5" },
164+
{ label: "\u20bf (BTC)", value: "\u20bf" },
165+
{ label: "None", value: "" }
166+
];
167+
168+
export function CurrencyCard({
169+
settings,
170+
onSave
171+
}: {
172+
settings: Settings | null;
173+
onSave: (symbol: string, denomination: Denomination) => Promise<void>;
174+
}) {
175+
const { t } = useTranslation();
176+
const curSymbol = settings?.currency?.symbol ?? "$";
177+
const curDenom: Denomination = settings?.currency?.denomination ?? "cents";
178+
179+
const [draftSymbol, setDraftSymbol] = useState<string>(curSymbol);
180+
const [draftDenom, setDraftDenom] = useState<Denomination>(curDenom);
181+
182+
React.useEffect(() => {
183+
setDraftSymbol(curSymbol);
184+
setDraftDenom(curDenom);
185+
}, [curSymbol, curDenom]);
186+
187+
const dirty = draftSymbol !== curSymbol || draftDenom !== curDenom;
188+
189+
// Example blind for preview
190+
const previewCents = 250;
191+
192+
return (
193+
<div className="card">
194+
<h3>{t("currency.sectionTitle")}</h3>
195+
<div className="muted">{t("currency.helpText")}</div>
196+
<hr />
197+
198+
{settings ? (
199+
<div style={{ display: "grid", gap: 12 }}>
200+
<div className="grid2">
201+
<div>
202+
<label>{t("currency.symbol")}</label>
203+
<select
204+
className="input"
205+
value={draftSymbol}
206+
onChange={(e) => setDraftSymbol(e.target.value)}
207+
>
208+
{CURRENCY_OPTIONS.map((opt) => (
209+
<option key={opt.value} value={opt.value}>
210+
{opt.label}
211+
</option>
212+
))}
213+
</select>
214+
</div>
215+
216+
<div>
217+
<label>{t("currency.denomination")}</label>
218+
<select
219+
className="input"
220+
value={draftDenom}
221+
onChange={(e) => setDraftDenom(e.target.value as Denomination)}
222+
>
223+
<option value="cents">{t("currency.denominationCents")}</option>
224+
<option value="whole">{t("currency.denominationWhole")}</option>
225+
</select>
226+
</div>
227+
</div>
228+
229+
<div>
230+
<div className="muted" style={{ marginBottom: 4 }}>{t("currency.preview")}</div>
231+
<div style={{ padding: "8px 12px", border: "1px solid rgba(255,255,255,0.18)", borderRadius: 8, display: "inline-block" }}>
232+
<MoneyDisplay
233+
cents={previewCents}
234+
size={24}
235+
currencySymbol={draftSymbol}
236+
denomination={draftDenom}
237+
/>
238+
</div>
239+
</div>
240+
241+
<div style={{ display: "flex", gap: 8 }}>
242+
<button
243+
className="btn primary"
244+
disabled={!dirty}
245+
onClick={async () => {
246+
await onSave(draftSymbol, draftDenom);
247+
}}
248+
>
249+
{t("currency.save")}
250+
</button>
251+
<button
252+
className="btn"
253+
disabled={!dirty}
254+
onClick={() => {
255+
setDraftSymbol(curSymbol);
256+
setDraftDenom(curDenom);
257+
}}
258+
>
259+
{t("levels.actions.discard")}
260+
</button>
261+
</div>
262+
</div>
263+
) : (
264+
<div className="muted">{t("common.loading")}</div>
265+
)}
266+
</div>
267+
);
268+
}
269+
158270
export function SettingsTab({
159271
settings,
160272
sounds,
161273
onSetSound,
162274
onPreviewSound,
163-
onSaveSeating
275+
onSaveSeating,
276+
onSaveCurrency
164277
}: {
165278
settings: Settings | null;
166279
sounds: string[];
167280
onSetSound: (cue: "transition" | "half" | "thirty" | "five" | "end", file: string | null) => Promise<void>;
168281
onPreviewSound: (file: string) => void;
169282
onSaveSeating: (minPlayersPerTable: number) => Promise<void>;
283+
onSaveCurrency: (symbol: string, denomination: Denomination) => Promise<void>;
170284
}) {
171285
return (
172286
<div className="row" style={{ marginTop: 12, display: "grid", gap: 12 }}>
173287
<SoundsCard settings={settings} sounds={sounds} onSetSound={onSetSound} onPreview={onPreviewSound} />
288+
<CurrencyCard settings={settings} onSave={onSaveCurrency} />
174289
<SeatingCard settings={settings} onSave={onSaveSeating} />
175290
</div>
176291
);

0 commit comments

Comments
 (0)