Update and restyle
This commit is contained in:
@@ -1,8 +1,15 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
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 } from "lucide-react";
|
||||
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 {
|
||||
@@ -26,6 +33,7 @@ interface Prize {
|
||||
id: string;
|
||||
name: string;
|
||||
points: number;
|
||||
quantity: number;
|
||||
}
|
||||
|
||||
interface Participant {
|
||||
@@ -35,6 +43,14 @@ interface Participant {
|
||||
prizes: Prize[];
|
||||
}
|
||||
|
||||
interface EditingPrize {
|
||||
participantId: number;
|
||||
prizeId: string;
|
||||
name: string;
|
||||
points: string;
|
||||
quantity: string;
|
||||
}
|
||||
|
||||
interface StoredData {
|
||||
stage: Stage;
|
||||
numParticipants: number;
|
||||
@@ -44,31 +60,40 @@ interface StoredData {
|
||||
totalPoints: number;
|
||||
}
|
||||
|
||||
type Stage = 'setup' | 'points' | 'tracking';
|
||||
type Stage = "setup" | "points" | "tracking";
|
||||
|
||||
const STORAGE_KEY = 'arcadeTrackerData';
|
||||
const STORAGE_KEY = "arcadeTrackerData";
|
||||
|
||||
const ArcadeTracker: React.FC = () => {
|
||||
const [stage, setStage] = useState<Stage>('setup');
|
||||
const [numParticipants, setNumParticipants] = useState<number>(2);
|
||||
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 [participantSetup, setParticipantSetup] = useState<ParticipantSetup[]>([]);
|
||||
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 [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([
|
||||
{ id: 1, name: 'Player 1' },
|
||||
{ id: 2, name: 'Player 2' }
|
||||
]);
|
||||
setParticipantSetup(Array(numParticipants)
|
||||
.fill(null)
|
||||
.map((_, i) => ({
|
||||
id: i + 1,
|
||||
name: `Player ${i + 1}`,
|
||||
}))
|
||||
);
|
||||
}
|
||||
}, [participantSetup.length]); // Add the dependency
|
||||
}, [participantSetup.length, numParticipants]);
|
||||
|
||||
useEffect(() => {
|
||||
const savedData = localStorage.getItem(STORAGE_KEY);
|
||||
@@ -82,7 +107,7 @@ const ArcadeTracker: React.FC = () => {
|
||||
setParticipants(parsed.participants);
|
||||
setTotalPoints(parsed.totalPoints);
|
||||
} catch (error) {
|
||||
console.error('Error parsing saved data:', error);
|
||||
console.error("Error parsing saved data:", error);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
@@ -97,11 +122,18 @@ const ArcadeTracker: React.FC = () => {
|
||||
totalPoints,
|
||||
};
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(dataToSave));
|
||||
}, [stage, numParticipants, pointInputs, participants, totalPoints, participantSetup]); // Add participantSetup
|
||||
}, [
|
||||
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)
|
||||
setParticipantSetup((prev) =>
|
||||
prev.map((p) => (p.id === id ? { ...p, name } : p))
|
||||
);
|
||||
};
|
||||
|
||||
@@ -110,10 +142,12 @@ const ArcadeTracker: React.FC = () => {
|
||||
setNumParticipants(newNum);
|
||||
|
||||
// Update participant setup array
|
||||
setParticipantSetup(prev => {
|
||||
const newSetup = Array(newNum).fill(null).map((_, i) => ({
|
||||
setParticipantSetup((prev) => {
|
||||
const newSetup = Array(newNum)
|
||||
.fill(null)
|
||||
.map((_, i) => ({
|
||||
id: i + 1,
|
||||
name: prev[i]?.name || `Player ${i + 1}`
|
||||
name: prev[i]?.name || `Player ${i + 1}`,
|
||||
}));
|
||||
return newSetup;
|
||||
});
|
||||
@@ -125,78 +159,121 @@ const ArcadeTracker: React.FC = () => {
|
||||
|
||||
// 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;
|
||||
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 || []
|
||||
prizes: existingParticipant?.prizes || [],
|
||||
};
|
||||
});
|
||||
setTotalPoints(total);
|
||||
setParticipants(updatedParticipants);
|
||||
setStage('tracking');
|
||||
setStage("tracking");
|
||||
};
|
||||
|
||||
const handleAddPrize = (participantId: number) => {
|
||||
// Just add the prize, no validation
|
||||
if (editingPrize) {
|
||||
// We're editing an existing prize
|
||||
const points = parseInt(newPrizePoints) || 0;
|
||||
const quantity = parseInt(newPrizeQuantity) || 1;
|
||||
|
||||
setParticipants(current =>
|
||||
current.map(p => {
|
||||
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 - points,
|
||||
prizes: [
|
||||
...p.prizes,
|
||||
{
|
||||
id: Date.now().toString(),
|
||||
name: newPrizeName || 'Prize',
|
||||
points
|
||||
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;
|
||||
|
||||
// Clear inputs and close dialog
|
||||
setNewPrizeName('');
|
||||
setNewPrizePoints('');
|
||||
setEditingParticipantId(null);
|
||||
};
|
||||
|
||||
const removePrize = (participantId: number, prizeId: string) => {
|
||||
setParticipants(participants.map(p => {
|
||||
setParticipants((current) =>
|
||||
current.map((p) => {
|
||||
if (p.id === participantId) {
|
||||
const prizeToRemove = p.prizes.find(prize => prize.id === prizeId);
|
||||
return {
|
||||
...p,
|
||||
pointsAvailable: p.pointsAvailable + (prizeToRemove?.points || 0),
|
||||
prizes: p.prizes.filter(prize => prize.id !== prizeId)
|
||||
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;
|
||||
newInputs[index] = value === "" ? 0 : parseInt(value) || 0;
|
||||
setPointInputs(newInputs);
|
||||
};
|
||||
|
||||
const resetApp = () => {
|
||||
localStorage.removeItem(STORAGE_KEY);
|
||||
setStage('setup');
|
||||
setNumParticipants(2);
|
||||
setStage("setup");
|
||||
setNumParticipants(3);
|
||||
setParticipantSetup([
|
||||
{ id: 1, name: 'Player 1' },
|
||||
{ id: 2, name: 'Player 2' }
|
||||
{ id: 1, name: "Player 1" },
|
||||
{ id: 2, name: "Player 2" },
|
||||
{ id: 3, name: "Player 3" },
|
||||
]);
|
||||
setPointInputs([]);
|
||||
setParticipants([]);
|
||||
@@ -208,13 +285,12 @@ const ArcadeTracker: React.FC = () => {
|
||||
<div className="p-4 max-w-md mx-auto">
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle>Arcade Point Tracker</CardTitle>
|
||||
<CardDescription>Set up your group's point tracking</CardDescription>
|
||||
<CardTitle className="text-2xl">Arcade Point Tracker</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<Label>Number of Participants (2-10)</Label>
|
||||
<div>Number of participants:</div>
|
||||
<div className="flex items-center gap-4 mt-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -224,7 +300,7 @@ const ArcadeTracker: React.FC = () => {
|
||||
>
|
||||
<Minus className="h-4 w-4" />
|
||||
</Button>
|
||||
<div className="text-2xl font-medium w-8 text-center">
|
||||
<div className="text-xl font-medium w-8 text-center">
|
||||
{numParticipants}
|
||||
</div>
|
||||
<Button
|
||||
@@ -238,12 +314,14 @@ const ArcadeTracker: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<Label>Participant Names</Label>
|
||||
<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)}
|
||||
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"
|
||||
@@ -255,9 +333,9 @@ const ArcadeTracker: React.FC = () => {
|
||||
className="w-full"
|
||||
onClick={() => {
|
||||
setPointInputs(Array(numParticipants).fill(0));
|
||||
setStage('points');
|
||||
setStage("points");
|
||||
}}
|
||||
disabled={participantSetup.some(p => !p.name.trim())}
|
||||
disabled={participantSetup.some((p) => !p.name.trim())}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
@@ -271,20 +349,19 @@ const ArcadeTracker: React.FC = () => {
|
||||
<div className="p-4 max-w-md mx-auto">
|
||||
<Card className="shadow-lg">
|
||||
<CardHeader>
|
||||
<CardTitle>Enter Points</CardTitle>
|
||||
<CardDescription>Input the points from each card</CardDescription>
|
||||
<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}`}>Card {index + 1} Points</Label>
|
||||
<label htmlFor={`card-${index}`}>{participantSetup[index].name}'s points:</label>
|
||||
<Input
|
||||
key={`card-${index}`} // Key helps ensure clean re-render
|
||||
id={`card-${index}`}
|
||||
type="text" // Changed from number
|
||||
inputMode="numeric" // Still shows number keyboard on mobile
|
||||
value={pointInputs[index] === 0 ? '' : pointInputs[index]} // Empty if 0
|
||||
type="text"
|
||||
inputMode="numeric"
|
||||
value={pointInputs[index]}
|
||||
onChange={(e) => handlePointInput(index, e.target.value)}
|
||||
className="mt-1"
|
||||
placeholder="Enter points"
|
||||
@@ -294,19 +371,17 @@ const ArcadeTracker: React.FC = () => {
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="flex justify-between">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => setStage('setup')}
|
||||
>
|
||||
<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)}
|
||||
disabled={pointInputs.some((p) => !p)}
|
||||
>
|
||||
Calculate
|
||||
</Button> </CardFooter>
|
||||
</Button>{" "}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
@@ -321,43 +396,80 @@ const ArcadeTracker: React.FC = () => {
|
||||
<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, 0)
|
||||
, 0)}
|
||||
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')}
|
||||
onClick={() => setStage("points")}
|
||||
>
|
||||
Edit Points
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
Edit
|
||||
</Button>
|
||||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => setShowResetDialog(true)}
|
||||
>
|
||||
Reset App
|
||||
<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, 0)
|
||||
, 0) / totalPoints) * 100}
|
||||
className={`h-2 ${(participants.reduce((total, p) =>
|
||||
total + p.prizes.reduce((sum, prize) => sum + prize.points, 0)
|
||||
, 0) > totalPoints) ? '[&>div]:bg-destructive' : ''}`}
|
||||
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, 0)
|
||||
, 0) > totalPoints) ? 'text-destructive font-medium' : ''}`}>
|
||||
{((participants.reduce((total, p) =>
|
||||
total + p.prizes.reduce((sum, prize) => sum + prize.points, 0)
|
||||
, 0) / totalPoints) * 100).toFixed(1)}% used
|
||||
<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>
|
||||
@@ -367,7 +479,6 @@ const ArcadeTracker: React.FC = () => {
|
||||
|
||||
<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>
|
||||
@@ -377,11 +488,15 @@ const ArcadeTracker: React.FC = () => {
|
||||
Are you sure you want to reset all data? This action cannot be undone.
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => setShowResetDialog(false)}>
|
||||
<DialogFooter className="flex flex-row gap-2 w-full">
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setShowResetDialog(false)}
|
||||
className="flex-1"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button variant="destructive" onClick={resetApp}>
|
||||
<Button variant="default" onClick={resetApp} className="flex-1">
|
||||
Reset
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -404,14 +519,37 @@ const ArcadeTracker: React.FC = () => {
|
||||
</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>
|
||||
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' : ''}`}
|
||||
value={
|
||||
100 -
|
||||
(participant.pointsAvailable /
|
||||
(totalPoints / participants.length)) *
|
||||
100
|
||||
}
|
||||
className={`h-2 ${
|
||||
participant.pointsAvailable < 0
|
||||
? "[&>div]:bg-destructive"
|
||||
: ""
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</CardHeader>
|
||||
@@ -429,43 +567,139 @@ const ArcadeTracker: React.FC = () => {
|
||||
type: "spring",
|
||||
stiffness: 500,
|
||||
damping: 30,
|
||||
opacity: { duration: 0.2 }
|
||||
opacity: { duration: 0.2 },
|
||||
}}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<div className="flex justify-between items-center p-2 bg-secondary rounded">
|
||||
<div>
|
||||
<div className="font-medium">{prize.name}</div>
|
||||
<div className="text-sm text-muted-foreground">{prize.points} points</div>
|
||||
<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>
|
||||
"No prizes added yet. Click "Add Prize" to get started."
|
||||
<AlertDescription className="text-center">
|
||||
No prizes added yet
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<Dialog open={editingParticipantId === participant.id} onOpenChange={(open) => {
|
||||
if (!open) setEditingParticipantId(null);
|
||||
}}>
|
||||
<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)}
|
||||
onClick={() =>
|
||||
setEditingParticipantId(participant.id)
|
||||
}
|
||||
>
|
||||
<Plus className="h-4 w-4 mr-2" />
|
||||
Add Prize
|
||||
@@ -473,43 +707,82 @@ const ArcadeTracker: React.FC = () => {
|
||||
</DialogTrigger>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Prize for {participant.name}</DialogTitle>
|
||||
<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="space-y-2">
|
||||
<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)}
|
||||
onChange={(e) =>
|
||||
setNewPrizeName(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="prize-points">Points Cost</Label>
|
||||
<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)}
|
||||
onChange={(e) =>
|
||||
setNewPrizePoints(e.target.value)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<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"
|
||||
>
|
||||
Add Prize
|
||||
{editingPrize ? 'Save' : 'Add'} Prize
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
@@ -526,11 +799,11 @@ const ArcadeTracker: React.FC = () => {
|
||||
);
|
||||
|
||||
switch (stage) {
|
||||
case 'setup':
|
||||
case "setup":
|
||||
return renderSetupStage();
|
||||
case 'points':
|
||||
case "points":
|
||||
return renderPointsStage();
|
||||
case 'tracking':
|
||||
case "tracking":
|
||||
return renderTrackingStage();
|
||||
default:
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user