-
Notifications
You must be signed in to change notification settings - Fork 3
feat(events): high priority fixes β approval lock, sort, slug collision, computed status #21
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,41 @@ | ||
| # Events feature β next steps | ||
|
|
||
| Prioritas: **High**, **Medium**, **Low**. Ringkasan dari review fitur AI Events (listing, detail, submit, approval admin). | ||
|
|
||
| --- | ||
|
|
||
| ## High | ||
|
|
||
| 1. **Kunci halaman detail ke konten yang sudah disetujui** β Tambahkan filter `approved = true` pada `getEventBySlug` (dan pertimbangkan juga untuk related events / metadata OG), supaya event pending tidak bisa diakses lewat URL langsung. | ||
|
|
||
| 2. **Sambungkan kontrol βUrutkanβ (Terdekat / Terbaru) ke data** β Saat ini `selectedSort` tidak memengaruhi hasil; samakan perilaku dengan `getEvents` (mis. `latest` = `created_at` desc) atau tambahkan helper sort di client yang konsisten dengan server. | ||
|
|
||
| 3. **Tangani tabrakan `slug` dan error Supabase yang jelas** β Insert bisa gagal jika slug duplikat; tangkap error unik, beri pesan UI (βnama sudah dipakai, ubah namaβ), atau gunakan strategi slug unik (suffix). | ||
|
|
||
| 4. **`status` event selaras dengan waktu** β Job ringan/cron atau computed saat read: set `past` / `upcoming` berdasarkan tanggal agar badge dan CTA konsisten tanpa edit manual. | ||
|
|
||
| --- | ||
|
|
||
| ## Medium | ||
|
|
||
| 5. **Rapikan `useEventForm`** β Hapus atau ganti βPhase 1 mockβ di `handleSubmit` agar tidak menyesatkan maintainer; satu jalur submit lewat server action saja, atau panggil `submitEvent` dari hook dengan kontrak yang jelas. | ||
|
|
||
| 6. **UX kartu & CTA** β Pisahkan βDetailβ vs βDaftarβ; atau di grid arahkan tombol utama ke `registrationUrl` (dengan `rel`/security) dan sediakan link sekunder ke detail β kurangi klik yang tidak perlu jika niatnya registrasi. | ||
|
|
||
| 7. **Form lengkap vs skema DB** β Kolom `end_date` / `end_time` ada di DB dan detail sudah memakai `formatEventDateRange`, tapi form submit belum; tambahkan field opsional untuk event multi-hari. | ||
|
|
||
| 8. **Notifikasi & transparansi untuk submitter** β Email atau in-app (mis. βsedang ditinjauβ / βdisetujuiβ) dan halaman βevent sayaβ read-only supaya pengguna tidak harus menebak setelah submit. | ||
|
|
||
| --- | ||
|
|
||
| ## Low | ||
|
|
||
| 9. **Uji otomatis & konsistensi RLS** β Playwright untuk alur list β detail β submit β approve; plus tes bahwa policy Supabase selaras dengan asumsi app (anon tidak baca pending, dll.). | ||
|
|
||
| 10. **`lib/events-utils` mock vs nyata** β Deprecate atau hapus `getEventBySlug` / `getRelatedEvents` dari mock jika tidak dipakai; dokumentasikan satu sumber kebenaran (Supabase) untuk menghindari regressi dokumentasi (mis. file `.kiro` yang masih mengacu mock). | ||
|
|
||
| --- | ||
|
|
||
| ## Urutan kerja yang disarankan | ||
|
|
||
| Mulai dari **(1) + (2) + (3)** untuk dampak besar dengan risiko relatif terkendali. | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -5,7 +5,18 @@ import { ROLES } from '@/lib/actions/admin/schemas' | |||||||
| import { validateEventForm } from '@/lib/event-form-utils' | ||||||||
| import { createAdminClient } from '@/lib/supabase/admin' | ||||||||
| import { createClient } from '@/lib/supabase/server' | ||||||||
| import type { AIEvent, EventCategory, EventFormData, EventLocationType } from '@/types/events' | ||||||||
| import type { AIEvent, EventCategory, EventFormData, EventLocationType, EventStatus } from '@/types/events' | ||||||||
|
|
||||||||
| function computeEventStatus(date: string): EventStatus { | ||||||||
| const eventDate = new Date(date) | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P1: Parsing a date-only string with Prompt for AI agents
Suggested change
|
||||||||
| eventDate.setHours(0, 0, 0, 0) | ||||||||
| const today = new Date() | ||||||||
| today.setHours(0, 0, 0, 0) | ||||||||
|
|
||||||||
| if (eventDate < today) return 'past' | ||||||||
| if (eventDate.getTime() === today.getTime()) return 'ongoing' | ||||||||
| return 'upcoming' | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Status computation ignores end date for multi-day eventsMedium Severity
Additional Locations (1)Reviewed by Cursor Bugbot for commit f872277. Configure here. |
||||||||
| } | ||||||||
|
|
||||||||
| // Helper to map DB result (snake_case) to AIEvent (camelCase) | ||||||||
| function mapEventFromDB(data: any): AIEvent { | ||||||||
|
|
@@ -24,7 +35,8 @@ function mapEventFromDB(data: any): AIEvent { | |||||||
| registrationUrl: data.registration_url, | ||||||||
| coverImage: data.cover_image, | ||||||||
| category: data.category as EventCategory, | ||||||||
| status: data.status, | ||||||||
| status: computeEventStatus(data.date), | ||||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Useful? React with πΒ / π. |
||||||||
| createdAt: data.created_at, | ||||||||
| } | ||||||||
| } | ||||||||
|
|
||||||||
|
|
@@ -100,7 +112,12 @@ export async function getEventBySlug(slug: string) { | |||||||
|
|
||||||||
| const supabase = await createClient() | ||||||||
|
|
||||||||
| const { data, error } = await supabase.from('events').select('*').eq('slug', sanitizedSlug).single() | ||||||||
| const { data, error } = await supabase | ||||||||
| .from('events') | ||||||||
| .select('*') | ||||||||
| .eq('slug', sanitizedSlug) | ||||||||
| .eq('approved', true) | ||||||||
| .single() | ||||||||
|
|
||||||||
| if (error) { | ||||||||
| console.error('Error fetching event by slug:', error) | ||||||||
|
|
@@ -168,10 +185,28 @@ export async function submitEvent(formData: EventFormData) { | |||||||
| submitted_by: user.id, // Use authenticated user ID | ||||||||
| } | ||||||||
|
|
||||||||
| const { error } = await supabase.from('events').insert(dbData) | ||||||||
| // Insert with unique slug handling (auto-suffix on collision) | ||||||||
| let currentSlug = dbData.slug | ||||||||
| let insertError: any = null | ||||||||
| const maxRetries = 5 | ||||||||
|
|
||||||||
| if (error) { | ||||||||
| console.error('Error submitting event:', error) | ||||||||
| for (let i = 0; i < maxRetries; i++) { | ||||||||
| const { error } = await supabase.from('events').insert({ ...dbData, slug: currentSlug }) | ||||||||
| if (!error) { | ||||||||
| insertError = null | ||||||||
| break | ||||||||
| } | ||||||||
| insertError = error | ||||||||
| // 23505 = unique violation (assume slug collision) | ||||||||
| if (error.code === '23505') { | ||||||||
| currentSlug = `${dbData.slug}-${i + 2}` | ||||||||
| continue | ||||||||
|
Comment on lines
+202
to
+203
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
On a Useful? React with πΒ / π. |
||||||||
| } | ||||||||
| break | ||||||||
| } | ||||||||
|
|
||||||||
| if (insertError) { | ||||||||
| console.error('Error submitting event:', insertError) | ||||||||
| return { success: false, error: 'Failed to submit event' } | ||||||||
| } | ||||||||
|
|
||||||||
|
|
||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -1,7 +1,18 @@ | ||||||
| import { createClient } from '@supabase/supabase-js' | ||||||
| import { unstable_cache } from 'next/cache' | ||||||
| import { getSupabaseConfig } from '@/lib/env-config' | ||||||
| import type { AIEvent, EventCategory, EventLocationType } from '@/types/events' | ||||||
| import type { AIEvent, EventCategory, EventLocationType, EventStatus } from '@/types/events' | ||||||
|
|
||||||
| function computeEventStatus(date: string): EventStatus { | ||||||
| const eventDate = new Date(date) | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Parsing a date-only string with Prompt for AI agents
Suggested change
|
||||||
| eventDate.setHours(0, 0, 0, 0) | ||||||
| const today = new Date() | ||||||
| today.setHours(0, 0, 0, 0) | ||||||
|
|
||||||
| if (eventDate < today) return 'past' | ||||||
| if (eventDate.getTime() === today.getTime()) return 'ongoing' | ||||||
| return 'upcoming' | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Duplicated
|
||||||
| } | ||||||
|
Comment on lines
+6
to
+15
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. π οΈ Refactor suggestion | π Major | β‘ Quick win Extract duplicated This function is duplicated verbatim in β»οΈ Suggested approachAdd to import type { EventStatus } from '@/types/events'
export function computeEventStatus(date: string): EventStatus {
const eventDate = new Date(date)
eventDate.setHours(0, 0, 0, 0)
const today = new Date()
today.setHours(0, 0, 0, 0)
if (eventDate < today) return 'past'
if (eventDate.getTime() === today.getTime()) return 'ongoing'
return 'upcoming'
}Then import from both π€ Prompt for AI Agents |
||||||
|
|
||||||
| interface EventRow { | ||||||
| id: string | ||||||
|
|
@@ -18,7 +29,7 @@ interface EventRow { | |||||
| registration_url: string | ||||||
| cover_image: string | ||||||
| category: EventCategory | ||||||
| status: AIEvent['status'] | ||||||
| created_at: string | null | ||||||
| } | ||||||
|
|
||||||
| function mapEventFromDB(data: EventRow): AIEvent { | ||||||
|
|
@@ -37,7 +48,8 @@ function mapEventFromDB(data: EventRow): AIEvent { | |||||
| registrationUrl: data.registration_url, | ||||||
| coverImage: data.cover_image, | ||||||
| category: data.category, | ||||||
| status: data.status, | ||||||
| status: computeEventStatus(data.date), | ||||||
| createdAt: data.created_at ?? undefined, | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||


There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: New docs file was added without updating
docs/README.md, which breaks the documented docs-index convention.Prompt for AI agents