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
3 changes: 2 additions & 1 deletion app/event/list/event-list-client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,10 +67,11 @@ export default function EventListClient({ initialEvents }: EventListClientProps)
persistViewMode(mode)
}

// Apply filters and sort to mock data
// Apply filters and sort to server-fetched data
const filteredEvents = applyFilters(initialEvents, {
category: selectedCategory,
locationType: selectedLocation,
sort: selectedSort,
})

return (
Expand Down
41 changes: 41 additions & 0 deletions docs/events-feature/next-steps.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Events feature β€” next steps

Copy link
Copy Markdown

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
Check if this issue is valid β€” if so, understand the root cause and fix it. At docs/events-feature/next-steps.md, line 1:

<comment>New docs file was added without updating `docs/README.md`, which breaks the documented docs-index convention.</comment>

<file context>
@@ -0,0 +1,41 @@
+# Events feature β€” next steps
+
+Prioritas: **High**, **Medium**, **Low**. Ringkasan dari review fitur AI Events (listing, detail, submit, approval admin).
</file context>


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.
47 changes: 41 additions & 6 deletions lib/actions/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Parsing a date-only string with new Date(date) can shift the event day by timezone, producing incorrect computed statuses. Construct the date in local time from components instead.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At lib/actions/events.ts, line 11:

<comment>Parsing a date-only string with `new Date(date)` can shift the event day by timezone, producing incorrect computed statuses. Construct the date in local time from components instead.</comment>

<file context>
@@ -5,7 +5,18 @@ import { ROLES } from '@/lib/actions/admin/schemas'
+import type { AIEvent, EventCategory, EventFormData, EventLocationType, EventStatus } from '@/types/events'
+
+function computeEventStatus(date: string): EventStatus {
+  const eventDate = new Date(date)
+  eventDate.setHours(0, 0, 0, 0)
+  const today = new Date()
</file context>
Suggested change
const eventDate = new Date(date)
const [year, month, day] = date.split('-').map(Number)
const eventDate = new Date(year, month - 1, day)

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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Status computation ignores end date for multi-day events

Medium Severity

computeEventStatus only considers the start date, ignoring endDate. Multi-day events (e.g., a 3-day conference) are marked 'past' the day after they start, even though they're still running. The end_date column exists in the DB, is mapped to endDate in mapEventFromDB, and the codebase already handles multi-day display via formatEventDateRange. The status override now replaces whatever the DB had, so there's no fallback for these events.

Additional Locations (1)
Fix in CursorΒ Fix in Web

Reviewed by Cursor Bugbot for commit f872277. Configure here.

}

// Helper to map DB result (snake_case) to AIEvent (camelCase)
function mapEventFromDB(data: any): AIEvent {
Expand All @@ -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),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve ongoing status across multi-day event ranges

mapEventFromDB now overwrites DB status with computeEventStatus(data.date), which only compares the start date to today. For events that span multiple days (end_date is populated), day 2+ will be marked past even while the event is still running, so detail-page state that depends on event.status (e.g. past-event CTA behavior) becomes incorrect.

Useful? React with πŸ‘Β / πŸ‘Ž.

createdAt: data.created_at,
}
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Attempt the last generated slug before exhausting retries

On a 23505 collision, the loop assigns a new suffix and continues, but on the final iteration that new value is never inserted because the loop exits immediately afterward. With maxRetries = 5, collisions on base, -2, -3, -4, and -5 return failure even if -6 is available, causing avoidable submission failures for common names.

Useful? React with πŸ‘Β / πŸ‘Ž.

}
break
}

if (insertError) {
console.error('Error submitting event:', insertError)
return { success: false, error: 'Failed to submit event' }
}

Expand Down
9 changes: 9 additions & 0 deletions lib/events-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ export interface EventFilters {
locationType?: EventLocationType | 'All'
startDate?: string
endDate?: string
sort?: 'nearest' | 'latest'
}

export function applyFilters(events: AIEvent[], filters: EventFilters): AIEvent[] {
Expand All @@ -149,5 +150,13 @@ export function applyFilters(events: AIEvent[], filters: EventFilters): AIEvent[
filtered = filterByDateRange(filtered, filters.startDate, filters.endDate)
}

if (filters.sort === 'latest') {
return [...filtered].sort((a, b) => {
const dateA = a.createdAt ? new Date(a.createdAt).getTime() : 0
const dateB = b.createdAt ? new Date(b.createdAt).getTime() : 0
return dateB - dateA
})
}

return sortByNearestDate(filtered)
}
18 changes: 15 additions & 3 deletions lib/server/events-public.ts
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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Parsing a date-only string with new Date(date) can misclassify event status by one day in non-UTC timezones. Parse as local date explicitly before comparing.

Prompt for AI agents
Check if this issue is valid β€” if so, understand the root cause and fix it. At lib/server/events-public.ts, line 7:

<comment>Parsing a date-only string with `new Date(date)` can misclassify event status by one day in non-UTC timezones. Parse as local date explicitly before comparing.</comment>

<file context>
@@ -1,7 +1,18 @@
+import type { AIEvent, EventCategory, EventLocationType, EventStatus } from '@/types/events'
+
+function computeEventStatus(date: string): EventStatus {
+  const eventDate = new Date(date)
+  eventDate.setHours(0, 0, 0, 0)
+  const today = new Date()
</file context>
Suggested change
const eventDate = new Date(date)
const eventDate = new Date(`${date}T00:00:00`)

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'

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated computeEventStatus across two files

Low Severity

computeEventStatus is identically defined in both lib/actions/events.ts and lib/server/events-public.ts. This duplication means any future fix (e.g., adding endDate support) needs to be applied in both places, risking inconsistency. A shared utility module could house this function.

Additional Locations (1)
Fix in CursorΒ Fix in Web

Reviewed by Cursor Bugbot for commit f872277. Configure here.

}
Comment on lines +6 to +15

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ› οΈ Refactor suggestion | 🟠 Major | ⚑ Quick win

Extract duplicated computeEventStatus to a shared utility.

This function is duplicated verbatim in lib/actions/events.ts (lines 10-19). Consider extracting it to a shared location (e.g., lib/events-utils.ts) to maintain a single source of truth for event status computation.

♻️ Suggested approach

Add to lib/events-utils.ts:

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 lib/server/events-public.ts and lib/actions/events.ts.

πŸ€– Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/server/events-public.ts` around lines 6 - 15, The computeEventStatus
function is duplicated; extract it into a single shared utility module (e.g.,
events-utils) as an exported function with the same signature and EventStatus
return type, then remove the local implementations and replace them with imports
of computeEventStatus in both places that currently define it (the server-side
and actions code) so both files call the shared computeEventStatus
implementation.


interface EventRow {
id: string
Expand All @@ -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 {
Expand All @@ -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,
}
}

Expand Down
1 change: 1 addition & 0 deletions types/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export interface AIEvent {
coverImage: string
category: EventCategory
status: EventStatus
createdAt?: string
}

export interface EventFormData extends Omit<AIEvent, 'id' | 'endDate' | 'endTime'> {
Expand Down
Loading