Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ interface Match {
status: MatchStatus;
hasPredictions: boolean;
result?: string;
multiplier: 1 | 2 | 3;
}

interface EventMeta {
Expand All @@ -53,6 +54,7 @@ const MOCK_MATCHES: Match[] = [
matchTime: new Date(Date.now() + 86400 * 1000).toISOString(),
status: "upcoming",
hasPredictions: false,
multiplier: 1,
},
{
id: "match-002",
Expand All @@ -61,6 +63,7 @@ const MOCK_MATCHES: Match[] = [
matchTime: new Date(Date.now() - 3600 * 1000).toISOString(),
status: "started",
hasPredictions: true,
multiplier: 1,
},
{
id: "match-003",
Expand All @@ -70,6 +73,7 @@ const MOCK_MATCHES: Match[] = [
status: "resolved",
hasPredictions: true,
result: "Team Sigma",
multiplier: 1,
},
];

Expand Down Expand Up @@ -150,6 +154,7 @@ export default function MatchManagementPage() {
matchTime: data.matchTime,
status: "upcoming",
hasPredictions: false,
multiplier: data.multiplier,
};
setMatches((prev) => [...prev, newMatch]);
}
Expand All @@ -162,6 +167,7 @@ export default function MatchManagementPage() {
matchTime: m.matchTime,
status: "upcoming",
hasPredictions: false,
multiplier: m.multiplier,
}));
setMatches((prev) => [...prev, ...newMatches]);
}
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/component/creator-events/AddMatchForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface MatchFormData {
teamA: string;
teamB: string;
matchTime: string;
multiplier: 1 | 2 | 3;
}

interface AddMatchFormProps {
Expand All @@ -25,6 +26,7 @@ export default function AddMatchForm({ onAddMatch }: AddMatchFormProps) {
const [teamA, setTeamA] = useState("");
const [teamB, setTeamB] = useState("");
const [matchTime, setMatchTime] = useState(nowPlusOneHour());
const [multiplier, setMultiplier] = useState<MatchFormData["multiplier"]>(1);
const [isSubmitting, setIsSubmitting] = useState(false);
const [errors, setErrors] = useState<Partial<MatchFormData & { form: string }>>({});

Expand All @@ -50,6 +52,10 @@ export default function AddMatchForm({ onAddMatch }: AddMatchFormProps) {
errs.matchTime = "Match time must be in the future.";
}

if (![1, 2, 3].includes(multiplier)) {
errs.multiplier = "Please select a valid bonus multiplier.";
}

setErrors(errs);
return Object.keys(errs).length === 0;
}
Expand All @@ -64,10 +70,12 @@ export default function AddMatchForm({ onAddMatch }: AddMatchFormProps) {
teamA: teamA.trim(),
teamB: teamB.trim(),
matchTime,
multiplier,
});
setTeamA("");
setTeamB("");
setMatchTime(nowPlusOneHour());
setMultiplier(1);
setErrors({});
} catch {
setErrors({ form: "Failed to add match. Please try again." });
Expand Down Expand Up @@ -153,6 +161,30 @@ export default function AddMatchForm({ onAddMatch }: AddMatchFormProps) {
)}
</div>

<div className="space-y-1">
<label
htmlFor="match-multiplier"
className="block text-sm font-medium text-slate-300"
>
Bonus Multiplier
</label>
<select
id="match-multiplier"
value={multiplier}
onChange={(e) =>
setMultiplier(Number(e.target.value) as MatchFormData["multiplier"])
}
className="w-full rounded-2xl border border-white/10 bg-slate-950/90 px-4 py-3 text-sm text-white outline-none transition focus:border-amber-400 focus:ring-2 focus:ring-amber-400/20"
>
<option value={1}>1× Normal points</option>
<option value={2}>2× Bonus points</option>
<option value={3}>3× Bonus points</option>
</select>
{errors.multiplier && (
<p className="text-xs text-rose-400">{errors.multiplier}</p>
)}
</div>

<div className="flex justify-end pt-1">
<Button
type="submit"
Expand Down
18 changes: 16 additions & 2 deletions frontend/src/component/creator-events/BulkMatchUpload.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ interface ParsedRow {
teamA: string;
teamB: string;
matchTime: string;
multiplier: 1 | 2 | 3;
error?: string;
}

Expand All @@ -26,8 +27,12 @@ function parseCSV(raw: string): ParsedRow[] {

return lines.map((line, idx) => {
const cols = line.split(",").map((c) => c.trim());
const [teamA = "", teamB = "", matchTime = ""] = cols;
const [teamA = "", teamB = "", matchTime = "", multiplierValue = "1"] = cols;
const errors: string[] = [];
const parsedMultiplier = Number(multiplierValue);
const multiplier = [1, 2, 3].includes(parsedMultiplier)
? (parsedMultiplier as 1 | 2 | 3)
: 1;

if (!teamA) errors.push("Team A is empty");
if (!teamB) errors.push("Team B is empty");
Expand All @@ -40,11 +45,15 @@ function parseCSV(raw: string): ParsedRow[] {
if (isNaN(dt.getTime())) errors.push("Invalid ISO 8601 date");
else if (dt <= new Date()) errors.push("Match time must be in the future");
}
if (multiplierValue && !["1", "2", "3"].includes(multiplierValue)) {
errors.push("Multiplier must be 1, 2, or 3");
}

return {
teamA,
teamB,
matchTime,
multiplier,
error: errors.length > 0 ? errors.join("; ") : undefined,
};
});
Expand Down Expand Up @@ -113,6 +122,7 @@ export default function BulkMatchUpload({
teamA: r.teamA,
teamB: r.teamB,
matchTime: r.matchTime,
multiplier: r.multiplier,
})),
);
setImportSuccess(true);
Expand Down Expand Up @@ -146,7 +156,7 @@ export default function BulkMatchUpload({
<p className="text-sm text-slate-400">
Upload a CSV with columns:{" "}
<code className="rounded bg-white/10 px-1.5 py-0.5 text-xs text-amber-300">
Team A, Team B, Match Time (ISO 8601)
Team A, Team B, Match Time (ISO 8601), Multiplier (optional)
</code>
</p>

Expand Down Expand Up @@ -203,6 +213,7 @@ export default function BulkMatchUpload({
<th className="px-4 py-2">Team A</th>
<th className="px-4 py-2">Team B</th>
<th className="px-4 py-2">Match Time</th>
<th className="px-4 py-2">Multiplier</th>
<th className="px-4 py-2">Status</th>
</tr>
</thead>
Expand All @@ -223,6 +234,9 @@ export default function BulkMatchUpload({
<td className="px-4 py-2 font-mono text-xs text-slate-300">
{row.matchTime || <span className="text-slate-500">—</span>}
</td>
<td className="px-4 py-2 text-slate-300">
{row.multiplier}×
</td>
<td className="px-4 py-2">
{row.error ? (
<span className="text-xs text-rose-400" title={row.error}>
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/component/creator-events/MatchList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ export default function MatchList({ matches, userJoined, onPredict }: MatchListP
Winner: {winner}
</span>
) : null}
{match.multiplier && match.multiplier > 1 ? (
<span className="inline-flex items-center gap-2 rounded-full bg-amber-400/10 px-3 py-1 text-amber-200">
⚡ {match.multiplier}x Points
</span>
) : null}
</div>
</div>

Expand Down
55 changes: 55 additions & 0 deletions frontend/src/context/CreatorEventsContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,13 @@ export interface CreatorEventMatch {
teamB: string;
matchTime: string;
outcome: MatchOutcome;
<<<<<<< Updated upstream
homeScore: number | null;
awayScore: number | null;
pointsMultiplier: number;
=======
multiplier?: 1 | 2 | 3;
>>>>>>> Stashed changes
}

export interface Participant {
Expand Down Expand Up @@ -179,11 +183,19 @@ export interface CreatorEventsContextValue {
) => Promise<{ eventId: string; inviteCode: string }>;
joinEvent: (inviteCode: string) => Promise<boolean>;
addMatch: (
<<<<<<< Updated upstream
input: AddMatchInput | string,
teamA?: string,
teamB?: string,
matchTime?: string,
details?: Partial<AddMatchInput>,
=======
eventId: string,
teamA: string,
teamB: string,
matchTime: string,
multiplier: 1 | 2 | 3,
>>>>>>> Stashed changes
) => Promise<string>;
submitPrediction: (
input: SubmitPredictionInput | string,
Expand Down Expand Up @@ -997,6 +1009,7 @@ export function CreatorEventsProvider({
);
if (!event || event.participants >= event.maxParticipants) return false;

<<<<<<< Updated upstream
updateEvent(event.id, {
joined: true,
participants: event.participants + 1,
Expand All @@ -1017,6 +1030,48 @@ export function CreatorEventsProvider({
},
],
}));
=======
const addMatch = useCallback(
async (
eventId: string,
teamA: string,
teamB: string,
matchTime: string,
multiplier: 1 | 2 | 3,
): Promise<string> => {
setIsLoading(true);
setError(null);
try {
const matchId = `match-${Date.now()}`;
const newMatch: CreatorEventMatch = {
id: matchId,
eventId,
teamA,
teamB,
matchTime,
outcome: "Pending",
multiplier,
};
setMatchesCache((prev) => ({
...prev,
[eventId]: [...(prev[eventId] ?? []), newMatch],
}));
setEventCache((prev) => {
const event = prev[eventId];
if (!event) return prev;
return {
...prev,
[eventId]: { ...event, matchesCount: event.matchesCount + 1 },
};
});
return matchId;
} finally {
setIsLoading(false);
}
},
[],
);
>>>>>>> Stashed changes

return true;
},
Expand Down