814 lines
31 KiB
TypeScript
814 lines
31 KiB
TypeScript
import React, { useState, useEffect } from "react";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Card,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardContent,
|
|
CardFooter,
|
|
CardDescription,
|
|
} from "@/components/ui/card";
|
|
import { Plus, Minus, Trash2, ArrowLeft, Pencil } from "lucide-react";
|
|
import { motion, AnimatePresence } from "framer-motion";
|
|
import { Alert, AlertDescription } from "@/components/ui/alert";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogFooter,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
DialogTrigger,
|
|
} from "@/components/ui/dialog";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Progress } from "@/components/ui/progress";
|
|
|
|
interface ParticipantSetup {
|
|
id: number;
|
|
name: string;
|
|
}
|
|
|
|
interface Prize {
|
|
id: string;
|
|
name: string;
|
|
points: number;
|
|
quantity: number;
|
|
}
|
|
|
|
interface Participant {
|
|
id: number;
|
|
name: string;
|
|
pointsAvailable: number;
|
|
prizes: Prize[];
|
|
}
|
|
|
|
interface EditingPrize {
|
|
participantId: number;
|
|
prizeId: string;
|
|
name: string;
|
|
points: string;
|
|
quantity: string;
|
|
}
|
|
|
|
interface StoredData {
|
|
stage: Stage;
|
|
numParticipants: number;
|
|
participantSetup: ParticipantSetup[];
|
|
pointInputs: number[];
|
|
participants: Participant[];
|
|
totalPoints: number;
|
|
}
|
|
|
|
type Stage = "setup" | "points" | "tracking";
|
|
|
|
const STORAGE_KEY = "arcadeTrackerData";
|
|
|
|
const ArcadeTracker: React.FC = () => {
|
|
const [stage, setStage] = useState<Stage>("setup");
|
|
const [numParticipants, setNumParticipants] = useState<number>(3);
|
|
const [pointInputs, setPointInputs] = useState<number[]>([]);
|
|
const [participants, setParticipants] = useState<Participant[]>([]);
|
|
const [totalPoints, setTotalPoints] = useState<number>(0);
|
|
const [newPrizeName, setNewPrizeName] = useState<string>("");
|
|
const [newPrizePoints, setNewPrizePoints] = useState<string>("");
|
|
const [newPrizeQuantity, setNewPrizeQuantity] = useState<string>("1");
|
|
const [participantSetup, setParticipantSetup] = useState<ParticipantSetup[]>(
|
|
[]
|
|
);
|
|
const [showResetDialog, setShowResetDialog] = useState(false);
|
|
const [editingParticipantId, setEditingParticipantId] = useState<
|
|
number | null
|
|
>(null);
|
|
const [editingPrize, setEditingPrize] = useState<EditingPrize | null>(null);
|
|
|
|
useEffect(() => {
|
|
// Initialize participant setup when component mounts
|
|
if (participantSetup.length === 0) {
|
|
setParticipantSetup(Array(numParticipants)
|
|
.fill(null)
|
|
.map((_, i) => ({
|
|
id: i + 1,
|
|
name: `Player ${i + 1}`,
|
|
}))
|
|
);
|
|
}
|
|
}, [participantSetup.length, numParticipants]);
|
|
|
|
useEffect(() => {
|
|
const savedData = localStorage.getItem(STORAGE_KEY);
|
|
if (savedData) {
|
|
try {
|
|
const parsed = JSON.parse(savedData) as StoredData;
|
|
setStage(parsed.stage);
|
|
setNumParticipants(parsed.numParticipants);
|
|
setParticipantSetup(parsed.participantSetup || []);
|
|
setPointInputs(parsed.pointInputs);
|
|
setParticipants(parsed.participants);
|
|
setTotalPoints(parsed.totalPoints);
|
|
} catch (error) {
|
|
console.error("Error parsing saved data:", error);
|
|
}
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
const dataToSave: StoredData = {
|
|
stage,
|
|
numParticipants,
|
|
participantSetup,
|
|
pointInputs,
|
|
participants,
|
|
totalPoints,
|
|
};
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave));
|
|
}, [
|
|
stage,
|
|
numParticipants,
|
|
pointInputs,
|
|
participants,
|
|
totalPoints,
|
|
participantSetup,
|
|
]); // Add participantSetup
|
|
|
|
const handleParticipantNameChange = (id: number, name: string) => {
|
|
setParticipantSetup((prev) =>
|
|
prev.map((p) => (p.id === id ? { ...p, name } : p))
|
|
);
|
|
};
|
|
|
|
const handleNumParticipantsChange = (change: number) => {
|
|
const newNum = Math.max(2, Math.min(10, numParticipants + change));
|
|
setNumParticipants(newNum);
|
|
|
|
// Update participant setup array
|
|
setParticipantSetup((prev) => {
|
|
const newSetup = Array(newNum)
|
|
.fill(null)
|
|
.map((_, i) => ({
|
|
id: i + 1,
|
|
name: prev[i]?.name || `Player ${i + 1}`,
|
|
}));
|
|
return newSetup;
|
|
});
|
|
};
|
|
|
|
const handleUpdatePoints = () => {
|
|
const total = pointInputs.reduce((sum, points) => sum + points, 0);
|
|
const pointsPerPerson = Math.floor(total / numParticipants);
|
|
|
|
// Update each participant's points while preserving prizes
|
|
const updatedParticipants = participantSetup.map((setup) => {
|
|
const existingParticipant = participants.find((p) => p.id === setup.id);
|
|
const currentPrizePoints =
|
|
existingParticipant?.prizes.reduce(
|
|
(sum, prize) => sum + prize.points,
|
|
0
|
|
) || 0;
|
|
|
|
return {
|
|
id: setup.id,
|
|
name: setup.name,
|
|
pointsAvailable: pointsPerPerson - currentPrizePoints,
|
|
prizes: existingParticipant?.prizes || [],
|
|
};
|
|
});
|
|
setTotalPoints(total);
|
|
setParticipants(updatedParticipants);
|
|
setStage("tracking");
|
|
};
|
|
|
|
const handleAddPrize = (participantId: number) => {
|
|
if (editingPrize) {
|
|
// We're editing an existing prize
|
|
const points = parseInt(newPrizePoints) || 0;
|
|
const quantity = parseInt(newPrizeQuantity) || 1;
|
|
|
|
setParticipants((current) =>
|
|
current.map((p) => {
|
|
if (p.id === participantId) {
|
|
const oldPrize = p.prizes.find((prize) => prize.id === editingPrize.prizeId);
|
|
const oldPoints = (oldPrize?.points || 0) * (oldPrize?.quantity || 1);
|
|
const newPoints = points * quantity;
|
|
|
|
return {
|
|
...p,
|
|
pointsAvailable: p.pointsAvailable + oldPoints - newPoints,
|
|
prizes: p.prizes.map((prize) =>
|
|
prize.id === editingPrize.prizeId
|
|
? {
|
|
...prize,
|
|
name: newPrizeName || "Prize",
|
|
points,
|
|
quantity,
|
|
}
|
|
: prize
|
|
),
|
|
};
|
|
}
|
|
return p;
|
|
})
|
|
);
|
|
} else {
|
|
// We're adding a new prize
|
|
const points = parseInt(newPrizePoints) || 0;
|
|
const quantity = parseInt(newPrizeQuantity) || 1;
|
|
|
|
setParticipants((current) =>
|
|
current.map((p) => {
|
|
if (p.id === participantId) {
|
|
return {
|
|
...p,
|
|
pointsAvailable: p.pointsAvailable - points * quantity,
|
|
prizes: [
|
|
...p.prizes,
|
|
{
|
|
id: Date.now().toString(),
|
|
name: newPrizeName || "Prize",
|
|
points,
|
|
quantity,
|
|
},
|
|
],
|
|
};
|
|
}
|
|
return p;
|
|
})
|
|
);
|
|
}
|
|
|
|
// Clear inputs and close dialog
|
|
setNewPrizeName("");
|
|
setNewPrizePoints("");
|
|
setNewPrizeQuantity("1");
|
|
setEditingParticipantId(null);
|
|
setEditingPrize(null);
|
|
};
|
|
|
|
const removePrize = (participantId: number, prizeId: string) => {
|
|
setParticipants(
|
|
participants.map((p) => {
|
|
if (p.id === participantId) {
|
|
const prizeToRemove = p.prizes.find((prize) => prize.id === prizeId);
|
|
return {
|
|
...p,
|
|
pointsAvailable: p.pointsAvailable + (prizeToRemove?.points || 0) * (prizeToRemove?.quantity || 1),
|
|
prizes: p.prizes.filter((prize) => prize.id !== prizeId),
|
|
};
|
|
}
|
|
return p;
|
|
})
|
|
);
|
|
};
|
|
|
|
const handlePointInput = (index: number, value: string) => {
|
|
const newInputs = [...pointInputs];
|
|
newInputs[index] = value === "" ? 0 : parseInt(value) || 0;
|
|
setPointInputs(newInputs);
|
|
};
|
|
|
|
const resetApp = () => {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
setStage("setup");
|
|
setNumParticipants(3);
|
|
setParticipantSetup([
|
|
{ id: 1, name: "Player 1" },
|
|
{ id: 2, name: "Player 2" },
|
|
{ id: 3, name: "Player 3" },
|
|
]);
|
|
setPointInputs([]);
|
|
setParticipants([]);
|
|
setTotalPoints(0);
|
|
setShowResetDialog(false);
|
|
};
|
|
|
|
const renderSetupStage = () => (
|
|
<div className="p-4 max-w-md mx-auto">
|
|
<Card className="shadow-lg">
|
|
<CardHeader>
|
|
<CardTitle className="text-2xl">Arcade Point Tracker</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-6">
|
|
<div>
|
|
<div>Number of participants:</div>
|
|
<div className="flex items-center gap-4 mt-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => handleNumParticipantsChange(-1)}
|
|
disabled={numParticipants <= 2}
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</Button>
|
|
<div className="text-xl font-medium w-8 text-center">
|
|
{numParticipants}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => handleNumParticipantsChange(1)}
|
|
disabled={numParticipants >= 10}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-4">
|
|
<div>Enter participant names:</div>
|
|
{participantSetup.map((p, index) => (
|
|
<div key={p.id} className="flex gap-2">
|
|
<Input
|
|
value={p.name}
|
|
onChange={(e) =>
|
|
handleParticipantNameChange(p.id, e.target.value)
|
|
}
|
|
placeholder={`Player ${index + 1}`}
|
|
onClick={(e) => (e.target as HTMLInputElement).select()}
|
|
className="transition-colors focus:bg-accent focus:text-accent-foreground"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
<Button
|
|
className="w-full"
|
|
onClick={() => {
|
|
setPointInputs(Array(numParticipants).fill(0));
|
|
setStage("points");
|
|
}}
|
|
disabled={participantSetup.some((p) => !p.name.trim())}
|
|
>
|
|
Next
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
const renderPointsStage = () => (
|
|
<div className="p-4 max-w-md mx-auto">
|
|
<Card className="shadow-lg">
|
|
<CardHeader>
|
|
<CardTitle className="text-xl">Enter Points</CardTitle>
|
|
<CardDescription>Input the points on each participant's card</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{pointInputs.map((points, index) => (
|
|
<div key={index}>
|
|
<label htmlFor={`card-${index}`}>{participantSetup[index].name}'s points:</label>
|
|
<Input
|
|
id={`card-${index}`}
|
|
type="text"
|
|
inputMode="numeric"
|
|
value={pointInputs[index]}
|
|
onChange={(e) => handlePointInput(index, e.target.value)}
|
|
className="mt-1"
|
|
placeholder="Enter points"
|
|
/>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter className="flex justify-between">
|
|
<Button variant="outline" onClick={() => setStage("setup")}>
|
|
<ArrowLeft className="w-4 h-4 mr-2" />
|
|
Back
|
|
</Button>
|
|
<Button
|
|
onClick={handleUpdatePoints} // Changed from calculatePoints
|
|
disabled={pointInputs.some((p) => !p)}
|
|
>
|
|
Calculate
|
|
</Button>{" "}
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
);
|
|
|
|
const renderTrackingStage = () => (
|
|
<>
|
|
<div className="fixed top-0 left-0 right-0 z-10 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
|
<div className="max-w-md mx-auto p-4">
|
|
<Card className="shadow-lg">
|
|
<CardHeader className="space-y-2">
|
|
<div className="flex justify-between items-start gap-4">
|
|
<CardTitle className="flex flex-col gap-1">
|
|
<div>Total Points: {totalPoints}</div>
|
|
<div className="text-base font-normal text-muted-foreground">
|
|
Points Used:{" "}
|
|
{participants.reduce(
|
|
(total, p) =>
|
|
total +
|
|
p.prizes.reduce((sum, prize) => sum + prize.points * prize.quantity, 0),
|
|
0
|
|
)}
|
|
</div>
|
|
</CardTitle>
|
|
<div className="space-x-2 flex-shrink-0">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => setStage("points")}
|
|
>
|
|
<ArrowLeft className="h-4 w-4" />
|
|
Edit
|
|
</Button>
|
|
<Button
|
|
variant="destructive"
|
|
size="sm"
|
|
onClick={() => setShowResetDialog(true)}
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
Reset
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="space-y-1">
|
|
<Progress
|
|
value={
|
|
(participants.reduce(
|
|
(total, p) =>
|
|
total +
|
|
p.prizes.reduce((sum, prize) => sum + prize.points * prize.quantity, 0),
|
|
0
|
|
) /
|
|
totalPoints) *
|
|
100
|
|
}
|
|
className={`h-2 ${
|
|
participants.reduce(
|
|
(total, p) =>
|
|
total +
|
|
p.prizes.reduce((sum, prize) => sum + prize.points * prize.quantity, 0),
|
|
0
|
|
) > totalPoints
|
|
? "[&>div]:bg-destructive"
|
|
: ""
|
|
}`}
|
|
/>
|
|
<CardDescription
|
|
className={`text-right text-xs ${
|
|
participants.reduce(
|
|
(total, p) =>
|
|
total +
|
|
p.prizes.reduce((sum, prize) => sum + prize.points * prize.quantity, 0),
|
|
0
|
|
) > totalPoints
|
|
? "text-destructive font-medium"
|
|
: ""
|
|
}`}
|
|
>
|
|
{(
|
|
(participants.reduce(
|
|
(total, p) =>
|
|
total +
|
|
p.prizes.reduce((sum, prize) => sum + prize.points * prize.quantity, 0),
|
|
0
|
|
) /
|
|
totalPoints) *
|
|
100
|
|
).toFixed(1)}
|
|
% used
|
|
</CardDescription>
|
|
</div>
|
|
</CardHeader>
|
|
</Card>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="max-w-md mx-auto">
|
|
<div className="h-[150px]" /> {/* Spacer for fixed header */}
|
|
<div className="p-4 space-y-4">
|
|
<Dialog open={showResetDialog} onOpenChange={setShowResetDialog}>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>Reset Application</DialogTitle>
|
|
<DialogDescription>
|
|
Are you sure you want to reset all data? This action cannot be undone.
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<DialogFooter className="flex flex-row gap-2 w-full">
|
|
<Button
|
|
variant="secondary"
|
|
onClick={() => setShowResetDialog(false)}
|
|
className="flex-1"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button variant="default" onClick={resetApp} className="flex-1">
|
|
Reset
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<AnimatePresence>
|
|
{participants.map((participant, index) => (
|
|
<motion.div
|
|
key={participant.id}
|
|
initial={{ opacity: 0, y: 20 }}
|
|
animate={{ opacity: 1, y: 0 }}
|
|
exit={{ opacity: 0, y: -20 }}
|
|
transition={{ duration: 0.2, delay: index * 0.1 }}
|
|
>
|
|
<Card className="shadow-lg">
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<CardTitle>{participant.name}</CardTitle>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<div className="text-sm text-muted-foreground flex justify-between items-center">
|
|
<span>
|
|
Points Available: {participant.pointsAvailable} /{" "}
|
|
{Math.floor(totalPoints / participants.length)}
|
|
</span>
|
|
<span
|
|
className={`text-xs ${
|
|
participant.pointsAvailable < 0
|
|
? "text-destructive font-medium"
|
|
: ""
|
|
}`}
|
|
>
|
|
{(
|
|
(participant.pointsAvailable /
|
|
(totalPoints / participants.length)) *
|
|
100
|
|
).toFixed(1)}
|
|
%
|
|
</span>
|
|
</div>
|
|
<Progress
|
|
value={
|
|
100 -
|
|
(participant.pointsAvailable /
|
|
(totalPoints / participants.length)) *
|
|
100
|
|
}
|
|
className={`h-2 ${
|
|
participant.pointsAvailable < 0
|
|
? "[&>div]:bg-destructive"
|
|
: ""
|
|
}`}
|
|
/>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent className="pt-0">
|
|
<div className="space-y-2">
|
|
<AnimatePresence mode="popLayout">
|
|
{participant.prizes.map((prize) => (
|
|
<motion.div
|
|
key={`${participant.id}-${prize.id}`}
|
|
layout
|
|
initial={{ opacity: 0, height: 0 }}
|
|
animate={{ opacity: 1, height: "auto" }}
|
|
exit={{ opacity: 0, height: 0 }}
|
|
transition={{
|
|
type: "spring",
|
|
stiffness: 500,
|
|
damping: 30,
|
|
opacity: { duration: 0.2 },
|
|
}}
|
|
className="overflow-hidden"
|
|
>
|
|
<Card className="bg-secondary">
|
|
<CardContent className="flex justify-between items-center p-2">
|
|
{editingPrize?.prizeId === prize.id ? (
|
|
<div className="flex-1 flex gap-4 items-center">
|
|
<div className="w-1/2">
|
|
<Input
|
|
value={editingPrize.name}
|
|
onChange={(e) =>
|
|
setEditingPrize({ ...editingPrize, name: e.target.value })
|
|
}
|
|
className="h-8"
|
|
/>
|
|
</div>
|
|
<div className="w-1/4">
|
|
<Input
|
|
type="text"
|
|
inputMode="numeric"
|
|
value={editingPrize.points}
|
|
onChange={(e) =>
|
|
setEditingPrize({ ...editingPrize, points: e.target.value })
|
|
}
|
|
className="h-8"
|
|
/>
|
|
</div>
|
|
<div className="flex items-center gap-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => {
|
|
const current = parseInt(editingPrize.quantity) || 1;
|
|
setEditingPrize({
|
|
...editingPrize,
|
|
quantity: Math.max(1, current - 1).toString(),
|
|
});
|
|
}}
|
|
disabled={parseInt(editingPrize.quantity) <= 1}
|
|
>
|
|
<Minus className="h-3 w-3" />
|
|
</Button>
|
|
<span className="w-4 text-center">{editingPrize.quantity}</span>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
className="h-8 w-8"
|
|
onClick={() => {
|
|
const current = parseInt(editingPrize.quantity) || 1;
|
|
setEditingPrize({
|
|
...editingPrize,
|
|
quantity: (current + 1).toString(),
|
|
});
|
|
}}
|
|
>
|
|
<Plus className="h-3 w-3" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<div className="flex gap-2 items-baseline">
|
|
<div className="font-medium text-md">{prize.name}</div>
|
|
<div className="text-sm text-muted-foreground">
|
|
{prize.quantity > 1 ? `${prize.quantity}x ` : ''}{prize.points} points
|
|
{prize.quantity > 1 ? ` (${prize.points * prize.quantity} total)` : ''}
|
|
</div>
|
|
</div>
|
|
)}
|
|
<div className="flex gap-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => {
|
|
setEditingParticipantId(participant.id);
|
|
setEditingPrize({
|
|
participantId: participant.id,
|
|
prizeId: prize.id,
|
|
name: prize.name,
|
|
points: prize.points.toString(),
|
|
quantity: prize.quantity.toString(),
|
|
});
|
|
setNewPrizeName(prize.name);
|
|
setNewPrizePoints(prize.points.toString());
|
|
setNewPrizeQuantity(prize.quantity.toString());
|
|
}}
|
|
className="h-8 w-8"
|
|
>
|
|
<Pencil className="h-4 w-4" />
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => removePrize(participant.id, prize.id)}
|
|
className="h-8 w-8"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
|
|
{!participant.prizes.length && (
|
|
<Alert variant="default" className="bg-muted">
|
|
<AlertDescription className="text-center">
|
|
No prizes added yet
|
|
</AlertDescription>
|
|
</Alert>
|
|
)}
|
|
|
|
<Dialog
|
|
open={editingParticipantId === participant.id}
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setEditingParticipantId(null);
|
|
setEditingPrize(null);
|
|
setNewPrizeName("");
|
|
setNewPrizePoints("");
|
|
setNewPrizeQuantity("1");
|
|
}
|
|
}}
|
|
>
|
|
<DialogTrigger asChild>
|
|
<Button
|
|
className="w-full"
|
|
variant="outline"
|
|
onClick={() =>
|
|
setEditingParticipantId(participant.id)
|
|
}
|
|
>
|
|
<Plus className="h-4 w-4 mr-2" />
|
|
Add Prize
|
|
</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>
|
|
{editingPrize ? 'Edit' : 'Add'} Prize for {participant.name}
|
|
</DialogTitle>
|
|
<DialogDescription>
|
|
Available Points: {participant.pointsAvailable}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
<div className="space-y-4 py-4">
|
|
<div className="flex gap-4">
|
|
<div className="w-2/3 space-y-2">
|
|
<Label htmlFor="prize-name">Prize Name</Label>
|
|
<Input
|
|
id="prize-name"
|
|
value={newPrizeName}
|
|
onChange={(e) =>
|
|
setNewPrizeName(e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="w-1/3 space-y-2">
|
|
<Label htmlFor="prize-points">Points</Label>
|
|
<Input
|
|
id="prize-points"
|
|
type="text"
|
|
inputMode="numeric"
|
|
value={newPrizePoints}
|
|
onChange={(e) =>
|
|
setNewPrizePoints(e.target.value)
|
|
}
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<Label>Quantity</Label>
|
|
<div className="flex items-center gap-4 mt-2">
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => {
|
|
const current = parseInt(newPrizeQuantity) || 1;
|
|
setNewPrizeQuantity(Math.max(1, current - 1).toString());
|
|
}}
|
|
disabled={parseInt(newPrizeQuantity) <= 1}
|
|
>
|
|
<Minus className="h-4 w-4" />
|
|
</Button>
|
|
<div className="text-xl font-medium w-8 text-center">
|
|
{newPrizeQuantity}
|
|
</div>
|
|
<Button
|
|
variant="outline"
|
|
size="icon"
|
|
onClick={() => {
|
|
const current = parseInt(newPrizeQuantity) || 1;
|
|
setNewPrizeQuantity((current + 1).toString());
|
|
}}
|
|
>
|
|
<Plus className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<DialogFooter className="flex flex-row gap-2 w-full ">
|
|
<Button
|
|
variant="outline"
|
|
onClick={() => setEditingParticipantId(null)}
|
|
className="flex-1"
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
onClick={() => handleAddPrize(participant.id)}
|
|
disabled={!newPrizeName}
|
|
className="flex-1"
|
|
>
|
|
{editingPrize ? 'Save' : 'Add'} Prize
|
|
</Button>
|
|
</DialogFooter>
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</motion.div>
|
|
))}
|
|
</AnimatePresence>
|
|
</div>
|
|
</div>
|
|
</>
|
|
);
|
|
|
|
switch (stage) {
|
|
case "setup":
|
|
return renderSetupStage();
|
|
case "points":
|
|
return renderPointsStage();
|
|
case "tracking":
|
|
return renderTrackingStage();
|
|
default:
|
|
return null;
|
|
}
|
|
};
|
|
|
|
export default ArcadeTracker;
|