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