Update and restyle

This commit is contained in:
2025-01-18 21:40:15 -05:00
parent 887b8c5919
commit 1c3dbdd7f5

View File

@@ -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&apos;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&apos;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}&apos;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>
&quot;No prizes added yet. Click &quot;Add Prize&quot; to get started.&quot;
<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;