- Fix breadcrumb/page title positioning di Wizard
- Fix Import Analytics tidak refresh setelah delete
- Fix browser lag saat Confirm Import (mass insert)
- Add optional image untuk Prize
Sebelum mengerjakan setiap task:
- Buat plan - files yang akan dimodifikasi, approach, potensi impact
- Tunggu approval dari user
- Setelah approved, baru eksekusi
Current behavior:
- "Back to Events" dan "Create New Event" menempel ke sisi kiri browser
- Tidak selaras dengan form container yang centered
Expected behavior:
- Breadcrumb dan title selaras dengan form container (max-width 832px, centered)
Solution: Wrap dalam container yang sama dengan form (max-width 832px, centered).
// EventWizard.tsx layout structure
<div className="min-h-screen bg-surface-alt">
<Header />
<main className="container mx-auto px-5 py-8">
{/* Breadcrumb & Title - same max-width as form */}
<div className="max-w-[832px] mx-auto mb-6">
<button className="text-sm text-content-muted hover:text-primary">
← Back to Events
</button>
<h1 className="text-2xl font-bold text-navy mt-2">
{mode === 'edit' ? 'Edit Event' : 'Create New Event'}
</h1>
</div>
{/* Stepper */}
<WizardStepper ... />
{/* Form Container */}
<div className="max-w-[832px] mx-auto bg-white rounded-xl shadow-card p-8">
{/* form content */}
</div>
</main>
</div>Current behavior:
- Delete participant/coupon di Step Participants
- Import Analytics (total participants, total coupons) tidak update
- Harus manual refresh browser untuk melihat perubahan
Expected behavior:
- Setelah delete, analytics langsung update tanpa refresh
Solution: Invalidate query atau refetch setelah delete.
// Setelah delete participant
const handleDeleteParticipant = async () => {
await deleteParticipant(eventId, participantId);
// Invalidate queries untuk refresh data
queryClient.invalidateQueries(['event', eventId]);
queryClient.invalidateQueries(['participants', eventId]);
queryClient.invalidateQueries(['coupons', eventId]);
};
// Atau jika analytics di-fetch dari Event object
// Pastikan Event.totalParticipants dan Event.totalCoupons di-update saat deleteCek juga: Apakah Event.totalParticipants dan Event.totalCoupons di-update saat delete? Jika tidak, perlu update:
// Di deleteParticipant function
async deleteParticipant(eventId: string, participantId: string) {
const participant = await db.participants.get([eventId, participantId]);
const couponCount = participant?.couponCount || 0;
// Delete participant & coupons
await db.participants.delete([eventId, participantId]);
await db.coupons.where({ eventId, participantId }).delete();
// Update event counts
await db.events.where('id').equals(eventId).modify(event => {
event.totalParticipants = (event.totalParticipants || 0) - 1;
event.totalCoupons = (event.totalCoupons || 0) - couponCount;
});
}Current behavior:
- Klik "Confirm Import" → browser freeze beberapa detik
- UI tidak responsive selama proses import
Root cause:
- Mass insert ke IndexedDB dilakukan di main thread
- Blocking UI thread
Expected behavior:
- Import berjalan tanpa freeze browser
- Idealnya ada progress indicator
Solution:
Batch insert dengan chunks + yield ke main thread:
async function importParticipants(eventId: string, data: ParsedData[]) {
const BATCH_SIZE = 1000;
const total = data.length;
for (let i = 0; i < total; i += BATCH_SIZE) {
const batch = data.slice(i, i + BATCH_SIZE);
// Insert batch
await db.participants.bulkAdd(
batch.map(p => ({ ...p, eventId }))
);
// Yield ke main thread agar UI responsive
await new Promise(resolve => setTimeout(resolve, 0));
// Optional: update progress
onProgress?.((i + batch.length) / total * 100);
}
}Current behavior:
- Prize hanya memiliki name dan quantity
- Tidak ada opsi untuk menambahkan gambar
Expected behavior:
- Setiap prize bisa memiliki gambar (opsional)
- Gambar disimpan sebagai base64
- Ada preview dan opsi untuk ganti/hapus
Solution:
Schema update:
interface Prize {
id: string;
eventId: string;
name: string;
quantity: number;
order: number;
image?: string; // NEW: base64 atau URL
createdAt: Date;
updatedAt: Date;
}UI di Step Prizes:
┌─────────────────────────────────────────────────────────────────┐
│ Prize 1 [Remove] │
│ │
│ ┌──────────┐ Prize Name * │
│ │ │ ┌──────────────────────────────────────────┐ │
│ │ [Image] │ │ Grand Prize │ │
│ │ │ └──────────────────────────────────────────┘ │
│ │ + Upload │ │
│ └──────────┘ Quantity * │
│ ┌───────┐ │
│ │ 1 │ │
│ └───────┘ │
└─────────────────────────────────────────────────────────────────┘
Image Preview:
- 80x80px square
- border-radius: 8px
- border: 1px dashed #e6ebf1
- Jika belum ada: tampilkan icon + "Upload"
- Jika sudah ada: tampilkan preview + hover untuk ganti/hapus
Implementation:
// components/wizard/PrizeImageUpload.tsx
interface PrizeImageUploadProps {
value?: string;
onChange: (base64: string | undefined) => void;
}
export function PrizeImageUpload({ value, onChange }: PrizeImageUploadProps) {
const inputRef = useRef<HTMLInputElement>(null);
const handleFileChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
// Validate size (max 2MB)
if (file.size > 2 * 1024 * 1024) {
toast.error('Image must be less than 2MB');
return;
}
// Convert to base64
const reader = new FileReader();
reader.onload = () => {
onChange(reader.result as string);
};
reader.readAsDataURL(file);
};
return (
<div className="relative w-20 h-20">
<input
ref={inputRef}
type="file"
accept="image/*"
onChange={handleFileChange}
className="hidden"
/>
{value ? (
<div className="group relative w-full h-full">
<img
src={value}
alt="Prize"
className="w-full h-full object-cover rounded-lg border border-border"
/>
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity rounded-lg flex items-center justify-center gap-2">
<button onClick={() => inputRef.current?.click()}>
<Edit className="w-4 h-4 text-white" />
</button>
<button onClick={() => onChange(undefined)}>
<Trash className="w-4 h-4 text-white" />
</button>
</div>
</div>
) : (
<button
onClick={() => inputRef.current?.click()}
className="w-full h-full border border-dashed border-border rounded-lg flex flex-col items-center justify-center text-content-muted hover:border-primary hover:text-primary transition-colors"
>
<ImagePlus className="w-5 h-5" />
<span className="text-xs mt-1">Upload</span>
</button>
)}
</div>
);
}| File | Change |
|---|---|
src/pages/EventWizard.tsx |
Fix breadcrumb positioning |
src/components/wizard/StepParticipants.tsx |
Invalidate queries after delete |
src/repositories/dexie/participantRepository.ts |
Update event counts on delete |
src/services/importService.ts |
Batch import dengan yield |
src/types/index.ts |
Add image field to Prize |
src/components/wizard/PrizeImageUpload.tsx |
New component |
src/components/wizard/StepPrizes.tsx |
Integrate image upload |
1. Fix breadcrumb positioning (Issue 1)
↓
2. Fix analytics refresh after delete (Issue 2)
↓
3. Optimize import dengan batch + yield (Issue 3)
↓
4. Add prize image feature (Issue 4)
↓
5. Test & Build
- Breadcrumb & title aligned dengan form container
- Delete participant → analytics langsung update
- Delete coupon → analytics langsung update
- Import 100K+ data → UI tetap responsive, ada progress
- Prize image upload works
- Prize image preview shows correctly
- Prize image bisa dihapus/ganti
- Prize tanpa image tetap bisa disimpan