The reward system has been updated so that rewards are now assigned to the period instead of individual winners. This means all winners of a period will receive the same reward configured for that period.
- Added
reward_idfield toleaderboard_periodstable - Winners automatically inherit the reward from their period
POST /api/high_score/periods
// OLD
{
"gameId": "uuid",
"durationDays": 7,
"autoRestart": true
}
// NEW - Add rewardId field
{
"game_id": "uuid",
"durationDays": 7,
"autoRestart": true,
"rewardId": "uuid-of-reward" // Optional, can be null
}// Add reward selector to your create period form
const CreatePeriodForm = () => {
const [gameId, setGameId] = useState('');
const [durationDays, setDurationDays] = useState(7);
const [autoRestart, setAutoRestart] = useState(true);
const [rewardId, setRewardId] = useState(''); // NEW STATE
// Fetch available rewards
const { data: rewards } = useQuery('/api/reward/');
const handleSubmit = async (e) => {
e.preventDefault();
await createPeriod({
gameId,
durationDays,
autoRestart,
rewardId: rewardId || null // NEW FIELD
});
};
return (
<form onSubmit={handleSubmit}>
{/* Existing fields */}
{/* NEW: Reward selector */}
<div className="form-group">
<label>Reward for Winners</label>
<select
value={rewardId}
onChange={(e) => setRewardId(e.target.value)}
className="form-control"
>
<option value="">No reward</option>
{rewards?.map(reward => (
<option key={reward.id} value={reward.id}>
{reward.name} ({reward.required_seals} seals)
</option>
))}
</select>
</div>
<button type="submit">Create Period</button>
</form>
);
};PUT /api/high_score/periods/:id
// Now you can update the reward
{
"duration_days": 7, // Optional
"auto_restart": false, // Optional
"reward_id": "uuid" // Optional - NEW FIELD
}const EditPeriodForm = ({ periodId }) => {
const [updates, setUpdates] = useState({});
const [rewardId, setRewardId] = useState('');
const handleSubmit = async (e) => {
e.preventDefault();
await updatePeriod(periodId, {
...updates,
reward_id: rewardId || undefined // Only send if changed
});
};
return (
<form onSubmit={handleSubmit}>
{/* Other fields */}
<div className="form-group">
<label>Change Reward</label>
<select
value={rewardId}
onChange={(e) => setRewardId(e.target.value)}
>
<option value="">Keep current</option>
<option value="null">Remove reward</option>
{rewards?.map(reward => (
<option key={reward.id} value={reward.id}>
{reward.name}
</option>
))}
</select>
</div>
<button type="submit">Update Period</button>
</form>
);
};// GET /api/high_score/periods
{
"success": true,
"data": [
{
"id": "period-uuid",
"game_id": "game-uuid",
"start_date": "2025-01-01T00:00:00Z",
"end_date": "2025-01-07T23:59:59Z",
"duration_days": 7,
"is_active": true,
"auto_restart": true,
"reward_id": "reward-uuid",
"games": {
"name": "Game Name"
},
// NEW: Nested reward object
"rewards": {
"id": "reward-uuid",
"name": "Free Coffee",
"description": "One free coffee",
"image_url": "https://..."
}
}
]
}const PeriodsTable = () => {
const { data: periods } = useQuery('/api/high_score/periods');
return (
<table className="table">
<thead>
<tr>
<th>Game</th>
<th>Start Date</th>
<th>End Date</th>
<th>Status</th>
<th>Reward</th> {/* NEW COLUMN */}
<th>Actions</th>
</tr>
</thead>
<tbody>
{periods?.data?.map(period => (
<tr key={period.id}>
<td>{period.games?.name}</td>
<td>{formatDate(period.start_date)}</td>
<td>{formatDate(period.end_date)}</td>
<td>
{period.is_active ? (
<span className="badge badge-success">Active</span>
) : (
<span className="badge badge-secondary">Closed</span>
)}
</td>
{/* NEW: Display reward */}
<td>
{period.rewards ? (
<div className="d-flex align-items-center">
{period.rewards.image_url && (
<img
src={period.rewards.image_url}
alt={period.rewards.name}
style={{ width: 30, height: 30, marginRight: 8 }}
/>
)}
<span>{period.rewards.name}</span>
</div>
) : (
<span className="text-muted">No reward</span>
)}
</td>
<td>
<button onClick={() => editPeriod(period.id)}>Edit</button>
</td>
</tr>
))}
</tbody>
</table>
);
};The winners will automatically show the reward from their period. The existing endpoints work the same:
// GET /api/high_score/winners
// Response already includes reward info from the period
{
"data": [
{
"id": "winner-id",
"position": 1,
"final_score": 1000,
"claimed": false,
"rewards": { // This now comes from the period
"name": "Free Coffee",
"description": "One free coffee"
}
}
]
}The mobile app doesn't need any changes because:
- Winners automatically receive the period's reward
- The
/api/high_score/my-rewardsendpoint returns the same structure - Rewards are assigned automatically when a period closes
- Add reward selector to Create Period form
- Add
rewardIdfield to create period API call - Add reward selector to Edit Period form
- Add
reward_idfield to update period API call - Update periods table to show reward column
- Display reward image/name in periods list
- Test creating period with reward
- Test editing period reward
- Test periods with no reward (null)
- No changes needed - works automatically
- Simplified Management: Set reward once per period instead of per winner
- Consistency: All winners of a period get the same reward
- Automation: No manual assignment needed after period closes
- Inheritance: Auto-restarted periods inherit the reward from previous period
- Flexibility: Can still have periods without rewards (null)
-
Admin creates a new period
- Selects game
- Sets duration (7 days)
- Selects reward: "Free Coffee"
- Enables auto-restart
-
Period runs for 7 days
- Players compete
- Scores are tracked
-
Period closes automatically
- Top 3 winners are selected
- All 3 winners automatically receive "Free Coffee" reward
- No manual intervention needed
-
New period starts (if auto-restart enabled)
- Inherits "Free Coffee" as the reward
- Cycle continues
If you need help implementing these changes:
- Check the backend code in
/src/controllers/high_scoreController.js - Review the model changes in
/src/models/leaderboardPeriods.js - Test endpoints with Postman/Insomnia first
- Contact backend team for any API clarifications