feat(ui): add customer homepage - #60
Conversation
📝 WalkthroughWalkthroughThis PR introduces a new customer-facing event discovery page with search and pagination capabilities, refactors authentication UI layouts, simplifies the admin panel interface, and updates event visibility filtering for admin users. Multiple new customer components are added for event browsing, navigation, and footer display. Changes
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 2❌ Failed checks (2 warnings)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/routes/(admin)/admin/events/new/+page.svelte (1)
154-157:⚠️ Potential issue | 🟠 MajorEnforce the seat-map validation inside
handleSubmit(), not only in the button state.Lines 551-582 prevent the normal click path, but
handleSubmit()itself never checksvalidationIssues,sectionHasOverlap, orsectionDuplicatePrefixes. That means an invalid seat map can still be posted if submission reaches Line 294 through another path.🛡️ Minimal fix
async function handleSubmit() { + if (validationIssues.length > 0) { + toast.error(validationIssues[0]); + return; + } + const payload = buildPayload(); // Client-side validation const result = createEventSchema.safeParse(payload); if (!result.success) {Also applies to: 294-303, 551-582
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/routes/`(admin)/admin/events/new/+page.svelte around lines 154 - 157, The submission handler handleSubmit must enforce seat-map validation rather than relying only on the button state; modify handleSubmit to check validationIssues, sectionHasOverlap, and sectionDuplicatePrefixes at the start (or before any network/post call) and abort/return early if any are present, setting appropriate user-facing error state (e.g., reuse existing error variables or call the same validation report used for the button). Locate handleSectionValidation and the handleSubmit function and add the guard that mirrors the button-disabled logic (check validationIssues.length > 0 or sectionHasOverlap === true or sectionDuplicatePrefixes.length > 0) so no alternate code path can submit an invalid seat map. Ensure the function does not proceed to the existing submit/post logic when validation fails.
🧹 Nitpick comments (1)
src/lib/components/admin/event/SectionItem.svelte (1)
24-30: Unify remove signaling to avoid accidental double-handling.Line 25–Line 29 currently notify via both bubbling event and callback. If consumers subscribe to both, remove logic can run twice. Prefer a single contract (event-only or callback-only) for this component.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/components/admin/event/SectionItem.svelte` around lines 24 - 30, The component currently signals removal twice in handleRemove by both dispatching a bubbling CustomEvent('remove') via rootEl and calling the onremove callback prop; unify to a single contract to avoid double-handling. Pick one approach (preferred: event-only): remove the onremove invocation and any onremove prop handling so handleRemove only dispatches new CustomEvent('remove', { bubbles: true, detail: { index }}); alternatively, if you choose callback-only, remove the rootEl?.dispatchEvent call and keep onremove?.(); update the component API/docs and any consumers to match the chosen contract.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/lib/components/admin/layout/AdminHeader.svelte`:
- Around line 33-47: The icon-only buttons lack accessible names; add explicit
accessible labels to the mobile menu and logout buttons so screen readers can
announce them: update the Button using onclick={ontogglesidebar} to include an
aria-label like "Open menu" (or add a visually-hidden <span> label) and update
the Button using onclick={handleLogout} (and disabled={loggingOut}) to include
an aria-label like "Log out" (or a hidden label), ensuring the labels are
present even when the visible text is hidden and that they reference the
existing Button elements and the ontogglesidebar and handleLogout handlers.
In `@src/lib/components/customer/event/CategoryBar.svelte`:
- Around line 18-23: The category toggle buttons lack pressed-state semantics;
update the Button component usage in CategoryBar.svelte (the <Button ...
onclick={() => (activeCategory = cat.value)}> instances) to include
aria-pressed={activeCategory === cat.value} so screen readers receive the
button's selected state; ensure the attribute uses the same activeCategory and
cat.value expressions as the variant logic.
In `@src/lib/components/customer/event/EmptyState.svelte`:
- Line 8: The decorative emoji in the EmptyState.svelte component is announced
by screen readers; update the element with class "empty-icon" inside the
EmptyState component to mark it as purely decorative (e.g., add
aria-hidden="true" and ensure it's not focusable) so assistive tech ignores it;
locate the <div class="empty-icon"> in EmptyState.svelte and add the decorative
attributes accordingly.
In `@src/lib/components/customer/event/EventCard.svelte`:
- Around line 45-49: Parse event.eventDate once into a single Date object and
reuse it for both the day and month badge (e.g., create a parsedDate variable
inside the EventCard component) to avoid double parsing and
timezone/invalid-date drift; also check parsedDate for validity
(isNaN(parsedDate.getTime())) and handle invalid dates gracefully before calling
parsedDate.getDate() and parsedDate.toLocaleString('vi-VN', { month: 'short' }).
In `@src/lib/components/customer/event/Hero.svelte`:
- Around line 23-30: The decorative arrow SVGs in the Hero.svelte component are
currently semantic to assistive tech; update each arrow <svg> element (the CTA
arrow icons) to be non-semantic by adding aria-hidden="true" and optionally
focusable="false" so screen readers ignore them; apply the same change to the
other arrow SVG instance noted (the one around lines 50-52) to keep
accessibility consistent.
In `@src/lib/components/customer/event/Pagination.svelte`:
- Around line 31-38: The active pagination Button needs an aria-current
attribute so screen readers announce the current page: update the Button
instantiation in Pagination.svelte (the block that uses buildPageUrl(num),
currentPage and id "pagination-page-{num}") to include aria-current set to
'page' when num === currentPage (omit or set undefined otherwise) so only the
active page button has aria-current="page".
In `@src/lib/components/customer/layout/AuthNav.svelte`:
- Around line 23-24: The current form uses SvelteKit's use:enhance with
handleLogout but the /api/auth/logout endpoint is a +server.ts JSON endpoint, so
change to an explicit fetch-based submit: remove use:enhance from the <form> in
AuthNav.svelte and instead wire the form's on:submit to handleLogout (or update
handleLogout) so it preventDefault(), performs a fetch POST to
"/api/auth/logout" (including credentials/CSRF as needed), then runs the
existing invalidateAll()/goto flow on success; alternatively, if you want to
keep progressive enhancement, convert the endpoint to a form action in a
+page.server.ts and keep use:enhance, but do not use use:enhance with +server.ts
JSON endpoints.
In `@src/lib/components/customer/layout/CustomerFooter.svelte`:
- Around line 109-142: The App Store and Google Play CTA Buttons (Button with
id="footer-appstore" and Button with id="footer-googleplay") currently use
resolve('/') which incorrectly navigates to the homepage; update those Button
instances to either (a) point href to the real external store URLs (use full
https:// links and add target="_blank" rel="noopener noreferrer") or (b) if
listings are not ready, remove/omit the href and render a disabled state (e.g.,
add disabled or aria-disabled="true" and change the inner text to "Sắp ra mắt")
so they don't behave as links; ensure accessibility attributes reflect the
chosen state and keep the SVG and text blocks intact.
In `@src/lib/server/services/event.service.ts`:
- Line 129: The admin draft visibility check uses a truthy test "if (role ===
'admin' && userId)" which wrongly treats 0 as absent; update the condition to an
explicit nullish check such as "if (role === 'admin' && userId != null)" (or
"userId !== null && userId !== undefined") so that numeric userId values like 0
are handled correctly; locate the check in the admin visibility logic (the
condition referencing role === 'admin' and userId) in event.service.ts and
replace the truthy check with the nullish comparison.
In `@src/routes/`(admin)/admin/events/+page.svelte:
- Around line 18-20: The empty-state div is always rendered; restore the
data-driven branch so admins see their events: in +page.svelte stop rendering
the static placeholder unconditionally and wrap that div in a Svelte conditional
that checks the loaded events collection (use the page load output, e.g. export
let data and data.events or the local events variable returned by your load
function) so the empty message only appears when the collection is empty, and
ensure the existing events management UI (the {`#each` ...} list or component that
renders draft/published events) is rendered when data.events has items.
In `@src/routes/`(auth)/login/+page.svelte:
- Around line 114-119: The password toggle button currently has tabindex={-1}
which removes it from the keyboard tab order and makes the show/hide control
inaccessible; update the button (the element with onclick={() => (showPassword =
!showPassword)} and aria-label bound to showPassword) to be keyboard-focusable
by removing the tabindex attribute or setting tabindex={0} so it remains
reachable to keyboard users and retains the existing aria-label and click
handler.
In `@src/routes/`(auth)/register/+page.svelte:
- Around line 82-83: The outer wrapper div currently uses h-screen which
prevents scrolling on short viewports; update that wrapper (the div containing
Card.Root) to use min-h-screen instead of h-screen so the page can grow and
become scrollable when the form or virtual keyboard exceeds the viewport height,
keeping the Card.Root registration area reachable on phones.
- Around line 138-143: The password toggle button currently has tabindex={-1},
removing it from keyboard focus; remove the tabindex attribute (or change it to
tabindex={0}) on the button so it remains focusable, and ensure the toggle
wiring that uses showPassword (the button with onclick={() => (showPassword =
!showPassword)}) can be activated via keyboard (native button activation is fine
if you keep it a <button>), and keep the existing aria-label updating to reflect
the showPassword state.
In `@src/routes/`(customer)/+layout.svelte:
- Line 25: Remove the stray markup note "🧭 2️⃣ Navbar." from the layout
template in +layout.svelte so it no longer renders on every customer page;
locate the literal text "🧭 2️⃣ Navbar." in the file and delete it (or move it
to a code comment) to keep the note internal rather than part of the rendered
layout.
In `@src/routes/`(customer)/+page.server.ts:
- Around line 24-49: Compute totalEvents and totalPages before querying
eventList, then clamp the requested page (page) into the valid range [1,
totalPages] and recompute offset before running the eventList query;
specifically, move the COUNT query using db.select({ count: count()
}).from(events).where(and(...conditions)) ahead of the eventList query,
calculate totalPages = Math.ceil(totalEvents / limit), set page = Math.max(1,
Math.min(page, totalPages || 1)), recalc offset = (page - 1) * limit, and then
run the db.select()
.from(events).where(and(...conditions)).orderBy(desc(events.createdAt)).limit(limit).offset(offset)
to avoid rendering an empty state for out‑of‑range page values.
In `@src/routes/`(customer)/+page.svelte:
- Around line 10-17: The Event type and rendering need to support category
filtering: add a category (e.g., category: string) to the Event interface and
ensure incoming event objects populate it; create a derived variable (e.g.,
filteredEvents) that filters the existing events array by activeCategory (treat
a special "All" or empty value as no filter) and use filteredEvents in the
template where events are mapped/rendered (instead of raw events) and in the
empty-state check so switching activeCategory updates the displayed cards and
empty state accordingly.
- Around line 171-180: The .view-all-link rule uses an undefined CSS variable
--color-primary-dim; either change that usage to a theme token that actually
exists (e.g., replace --color-primary-dim with the existing primary token used
in your layout, such as --color-primary) in the .view-all-link selector, or add
a definition for --color-primary-dim in the layout’s :root/theme variables so
the value is defined globally; update the .view-all-link class (and/or the
layout variables) accordingly to ensure the link uses a defined color token.
In `@tsconfig.json`:
- Line 14: Remove "bun-types" from the global "types" array in tsconfig.json so
Bun APIs are not exposed to client/universal code, and instead create a
server-only ambient declaration file that adds Bun typings via a triple-slash
directive (e.g., a new .d.ts containing /// <reference types="bun-types" />) and
ensure only server/test compilation picks up that file (so tests like
seat-label.test.ts get Bun types but client code does not); update any
server/test-specific tsconfig or include patterns to reference that new .d.ts as
needed.
---
Outside diff comments:
In `@src/routes/`(admin)/admin/events/new/+page.svelte:
- Around line 154-157: The submission handler handleSubmit must enforce seat-map
validation rather than relying only on the button state; modify handleSubmit to
check validationIssues, sectionHasOverlap, and sectionDuplicatePrefixes at the
start (or before any network/post call) and abort/return early if any are
present, setting appropriate user-facing error state (e.g., reuse existing error
variables or call the same validation report used for the button). Locate
handleSectionValidation and the handleSubmit function and add the guard that
mirrors the button-disabled logic (check validationIssues.length > 0 or
sectionHasOverlap === true or sectionDuplicatePrefixes.length > 0) so no
alternate code path can submit an invalid seat map. Ensure the function does not
proceed to the existing submit/post logic when validation fails.
---
Nitpick comments:
In `@src/lib/components/admin/event/SectionItem.svelte`:
- Around line 24-30: The component currently signals removal twice in
handleRemove by both dispatching a bubbling CustomEvent('remove') via rootEl and
calling the onremove callback prop; unify to a single contract to avoid
double-handling. Pick one approach (preferred: event-only): remove the onremove
invocation and any onremove prop handling so handleRemove only dispatches new
CustomEvent('remove', { bubbles: true, detail: { index }}); alternatively, if
you choose callback-only, remove the rootEl?.dispatchEvent call and keep
onremove?.(); update the component API/docs and any consumers to match the
chosen contract.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 686a6495-9a5f-4967-8279-bde72baa651b
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (31)
.gitignorepackage.jsonsrc/lib/components/admin/event/SectionBuilder.sveltesrc/lib/components/admin/event/SectionItem.sveltesrc/lib/components/admin/layout/AdminHeader.sveltesrc/lib/components/admin/layout/AdminSidebar.sveltesrc/lib/components/customer/event/CategoryBar.sveltesrc/lib/components/customer/event/EmptyState.sveltesrc/lib/components/customer/event/EventCard.sveltesrc/lib/components/customer/event/Hero.sveltesrc/lib/components/customer/event/Pagination.sveltesrc/lib/components/customer/layout/AuthNav.sveltesrc/lib/components/customer/layout/CustomerFooter.sveltesrc/lib/components/customer/layout/CustomerNavbar.sveltesrc/lib/components/customer/layout/CustomerSearchBar.sveltesrc/lib/components/customer/layout/Logo.sveltesrc/lib/server/auth/jwt.tssrc/lib/server/services/event.service.tssrc/routes/(admin)/admin/+layout.sveltesrc/routes/(admin)/admin/events/+page.sveltesrc/routes/(admin)/admin/events/new/+page.sveltesrc/routes/(auth)/+layout.sveltesrc/routes/(auth)/login/+page.sveltesrc/routes/(auth)/register/+page.sveltesrc/routes/(customer)/+layout.sveltesrc/routes/(customer)/+page.server.tssrc/routes/(customer)/+page.sveltesrc/routes/+layout.server.tssrc/routes/+page.server.tssrc/routes/+page.sveltetsconfig.json
💤 Files with no reviewable changes (2)
- src/routes/+page.svelte
- src/routes/+page.server.ts
| <Button variant="ghost" size="icon" class="md:hidden" onclick={ontogglesidebar}> | ||
| <Menu class="h-5 w-5" /> | ||
| </Button> | ||
|
|
||
| <div class="flex flex-col"> | ||
| <h2 class="ml-2 font-heading text-lg font-bold tracking-tight text-foreground"> | ||
| {pageTitle} | ||
| </h2> | ||
| </div> | ||
| <h2 class="text-sm font-semibold text-foreground">Admin Panel</h2> | ||
| </div> | ||
|
|
||
| <div class="flex items-center gap-2"> | ||
| <DropdownMenu.Root> | ||
| <DropdownMenu.Trigger> | ||
| {#snippet child({ props })} | ||
| <button | ||
| {...props} | ||
| class="flex items-center gap-2.5 rounded-xl border border-border/50 bg-card px-3 py-1.5 hover:bg-accent/60" | ||
| style="transition: all 0.2s var(--ease-bento);" | ||
| aria-label="Tài khoản Admin" | ||
| > | ||
| <Avatar.Root class="h-7 w-7"> | ||
| <Avatar.Fallback class="bg-primary/10 text-xs font-semibold text-primary"> | ||
| A | ||
| </Avatar.Fallback> | ||
| </Avatar.Root> | ||
| <span class="hidden text-sm font-medium text-foreground md:inline">Admin</span> | ||
| </button> | ||
| {/snippet} | ||
| </DropdownMenu.Trigger> | ||
| <DropdownMenu.Content align="end" class="w-48 rounded-xl p-1.5"> | ||
| <DropdownMenu.Group> | ||
| <DropdownMenu.Item class="gap-2 rounded-lg" disabled> | ||
| <User class="h-4 w-4 text-muted-foreground" /> | ||
| <span>Hồ sơ</span> | ||
| </DropdownMenu.Item> | ||
| <DropdownMenu.Separator /> | ||
| <DropdownMenu.Item | ||
| class="gap-2 rounded-lg text-destructive focus:text-destructive" | ||
| onclick={handleLogout} | ||
| disabled={loggingOut} | ||
| > | ||
| {#if loggingOut} | ||
| <Loader class="h-4 w-4 animate-spin" /> | ||
| {:else} | ||
| <LogOut class="h-4 w-4" /> | ||
| {/if} | ||
| <span>Đăng xuất</span> | ||
| </DropdownMenu.Item> | ||
| </DropdownMenu.Group> | ||
| </DropdownMenu.Content> | ||
| </DropdownMenu.Root> | ||
| <div class="flex items-center gap-2 md:gap-3"> | ||
| <span class="hidden text-sm text-muted-foreground md:inline">👤 Admin</span> | ||
| <Button variant="ghost" size="sm" onclick={handleLogout} disabled={loggingOut}> | ||
| {#if loggingOut} | ||
| <Loader class="mr-1.5 h-3.5 w-3.5 animate-spin" /> | ||
| {:else} | ||
| <LogOut class="mr-1.5 h-3.5 w-3.5" /> | ||
| {/if} | ||
| <span class="hidden md:inline">Đăng xuất</span> |
There was a problem hiding this comment.
Add accessible names to the mobile icon buttons.
On small screens both controls are icon-only: the menu button has no label, and the logout text is hidden. Screen readers won't announce a meaningful name for either button.
Suggested tweak
- <Button variant="ghost" size="icon" class="md:hidden" onclick={ontogglesidebar}>
+ <Button
+ variant="ghost"
+ size="icon"
+ class="md:hidden"
+ onclick={ontogglesidebar}
+ aria-label="Mở menu điều hướng"
+ >
<Menu class="h-5 w-5" />
</Button>
@@
- <Button variant="ghost" size="sm" onclick={handleLogout} disabled={loggingOut}>
+ <Button
+ variant="ghost"
+ size="sm"
+ onclick={handleLogout}
+ disabled={loggingOut}
+ aria-label="Đăng xuất"
+ >📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <Button variant="ghost" size="icon" class="md:hidden" onclick={ontogglesidebar}> | |
| <Menu class="h-5 w-5" /> | |
| </Button> | |
| <div class="flex flex-col"> | |
| <h2 class="ml-2 font-heading text-lg font-bold tracking-tight text-foreground"> | |
| {pageTitle} | |
| </h2> | |
| </div> | |
| <h2 class="text-sm font-semibold text-foreground">Admin Panel</h2> | |
| </div> | |
| <div class="flex items-center gap-2"> | |
| <DropdownMenu.Root> | |
| <DropdownMenu.Trigger> | |
| {#snippet child({ props })} | |
| <button | |
| {...props} | |
| class="flex items-center gap-2.5 rounded-xl border border-border/50 bg-card px-3 py-1.5 hover:bg-accent/60" | |
| style="transition: all 0.2s var(--ease-bento);" | |
| aria-label="Tài khoản Admin" | |
| > | |
| <Avatar.Root class="h-7 w-7"> | |
| <Avatar.Fallback class="bg-primary/10 text-xs font-semibold text-primary"> | |
| A | |
| </Avatar.Fallback> | |
| </Avatar.Root> | |
| <span class="hidden text-sm font-medium text-foreground md:inline">Admin</span> | |
| </button> | |
| {/snippet} | |
| </DropdownMenu.Trigger> | |
| <DropdownMenu.Content align="end" class="w-48 rounded-xl p-1.5"> | |
| <DropdownMenu.Group> | |
| <DropdownMenu.Item class="gap-2 rounded-lg" disabled> | |
| <User class="h-4 w-4 text-muted-foreground" /> | |
| <span>Hồ sơ</span> | |
| </DropdownMenu.Item> | |
| <DropdownMenu.Separator /> | |
| <DropdownMenu.Item | |
| class="gap-2 rounded-lg text-destructive focus:text-destructive" | |
| onclick={handleLogout} | |
| disabled={loggingOut} | |
| > | |
| {#if loggingOut} | |
| <Loader class="h-4 w-4 animate-spin" /> | |
| {:else} | |
| <LogOut class="h-4 w-4" /> | |
| {/if} | |
| <span>Đăng xuất</span> | |
| </DropdownMenu.Item> | |
| </DropdownMenu.Group> | |
| </DropdownMenu.Content> | |
| </DropdownMenu.Root> | |
| <div class="flex items-center gap-2 md:gap-3"> | |
| <span class="hidden text-sm text-muted-foreground md:inline">👤 Admin</span> | |
| <Button variant="ghost" size="sm" onclick={handleLogout} disabled={loggingOut}> | |
| {#if loggingOut} | |
| <Loader class="mr-1.5 h-3.5 w-3.5 animate-spin" /> | |
| {:else} | |
| <LogOut class="mr-1.5 h-3.5 w-3.5" /> | |
| {/if} | |
| <span class="hidden md:inline">Đăng xuất</span> | |
| <Button | |
| variant="ghost" | |
| size="icon" | |
| class="md:hidden" | |
| onclick={ontogglesidebar} | |
| aria-label="Mở menu điều hướng" | |
| > | |
| <Menu class="h-5 w-5" /> | |
| </Button> | |
| <h2 class="text-sm font-semibold text-foreground">Admin Panel</h2> | |
| </div> | |
| <div class="flex items-center gap-2 md:gap-3"> | |
| <span class="hidden text-sm text-muted-foreground md:inline">👤 Admin</span> | |
| <Button | |
| variant="ghost" | |
| size="sm" | |
| onclick={handleLogout} | |
| disabled={loggingOut} | |
| aria-label="Đăng xuất" | |
| > | |
| {`#if` loggingOut} | |
| <Loader class="mr-1.5 h-3.5 w-3.5 animate-spin" /> | |
| {:else} | |
| <LogOut class="mr-1.5 h-3.5 w-3.5" /> | |
| {/if} | |
| <span class="hidden md:inline">Đăng xuất</span> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/admin/layout/AdminHeader.svelte` around lines 33 - 47, The
icon-only buttons lack accessible names; add explicit accessible labels to the
mobile menu and logout buttons so screen readers can announce them: update the
Button using onclick={ontogglesidebar} to include an aria-label like "Open menu"
(or add a visually-hidden <span> label) and update the Button using
onclick={handleLogout} (and disabled={loggingOut}) to include an aria-label like
"Log out" (or a hidden label), ensuring the labels are present even when the
visible text is hidden and that they reference the existing Button elements and
the ontogglesidebar and handleLogout handlers.
| <Button | ||
| id="cat-{cat.value || 'all'}" | ||
| variant={activeCategory === cat.value ? 'default' : 'outline'} | ||
| size="sm" | ||
| onclick={() => (activeCategory = cat.value)} | ||
| class="rounded-full whitespace-nowrap" |
There was a problem hiding this comment.
Add pressed-state semantics to category toggles.
These controls behave like a selectable toggle set. Add aria-pressed={activeCategory === cat.value} so state is announced accessibly.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/customer/event/CategoryBar.svelte` around lines 18 - 23,
The category toggle buttons lack pressed-state semantics; update the Button
component usage in CategoryBar.svelte (the <Button ... onclick={() =>
(activeCategory = cat.value)}> instances) to include
aria-pressed={activeCategory === cat.value} so screen readers receive the
button's selected state; ensure the attribute uses the same activeCategory and
cat.value expressions as the variant logic.
| </script> | ||
|
|
||
| <div class="empty-state"> | ||
| <div class="empty-icon">🎭</div> |
There was a problem hiding this comment.
Hide decorative emoji from assistive tech.
Line 8 should mark the icon as decorative to avoid noisy announcements by screen readers.
Suggested fix
- <div class="empty-icon">🎭</div>
+ <div class="empty-icon" aria-hidden="true">🎭</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="empty-icon">🎭</div> | |
| <div class="empty-icon" aria-hidden="true">🎭</div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/customer/event/EmptyState.svelte` at line 8, The
decorative emoji in the EmptyState.svelte component is announced by screen
readers; update the element with class "empty-icon" inside the EmptyState
component to mark it as purely decorative (e.g., add aria-hidden="true" and
ensure it's not focusable) so assistive tech ignores it; locate the <div
class="empty-icon"> in EmptyState.svelte and add the decorative attributes
accordingly.
| <div class="card-date-badge"> | ||
| <span class="date-day">{new Date(event.eventDate).getDate()}</span> | ||
| <span class="date-month"> | ||
| {new Date(event.eventDate).toLocaleString('vi-VN', { month: 'short' })} | ||
| </span> |
There was a problem hiding this comment.
Use a single parsed date source for the badge to avoid timezone/invalid-date drift.
Line 46 and Line 48 parse event.eventDate independently, which can diverge from the formatted date shown elsewhere and makes invalid values harder to handle consistently.
Suggested fix
<script lang="ts">
import { resolve } from '$app/paths';
import { Badge } from '$lib/components/ui/badge';
@@
let { event, i, formatDate, formatPrice, getGradient } = $props<{
@@
}>();
+
+ const badgeDate = $derived(new Date(event.eventDate));
+ const isBadgeDateValid = $derived(!Number.isNaN(badgeDate.getTime()));
</script>
@@
<!-- Date badge -->
<div class="card-date-badge">
- <span class="date-day">{new Date(event.eventDate).getDate()}</span>
- <span class="date-month">
- {new Date(event.eventDate).toLocaleString('vi-VN', { month: 'short' })}
- </span>
+ {`#if` isBadgeDateValid}
+ <span class="date-day">{badgeDate.getDate()}</span>
+ <span class="date-month">{badgeDate.toLocaleString('vi-VN', { month: 'short' })}</span>
+ {:else}
+ <span class="date-day">--</span>
+ <span class="date-month">---</span>
+ {/if}
</div>📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <div class="card-date-badge"> | |
| <span class="date-day">{new Date(event.eventDate).getDate()}</span> | |
| <span class="date-month"> | |
| {new Date(event.eventDate).toLocaleString('vi-VN', { month: 'short' })} | |
| </span> | |
| <div class="card-date-badge"> | |
| {`#if` isBadgeDateValid} | |
| <span class="date-day">{badgeDate.getDate()}</span> | |
| <span class="date-month">{badgeDate.toLocaleString('vi-VN', { month: 'short' })}</span> | |
| {:else} | |
| <span class="date-day">--</span> | |
| <span class="date-month">---</span> | |
| {/if} | |
| </div> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/customer/event/EventCard.svelte` around lines 45 - 49,
Parse event.eventDate once into a single Date object and reuse it for both the
day and month badge (e.g., create a parsedDate variable inside the EventCard
component) to avoid double parsing and timezone/invalid-date drift; also check
parsedDate for validity (isNaN(parsedDate.getTime())) and handle invalid dates
gracefully before calling parsedDate.getDate() and
parsedDate.toLocaleString('vi-VN', { month: 'short' }).
| <svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"> | ||
| <path | ||
| stroke-linecap="round" | ||
| stroke-linejoin="round" | ||
| stroke-width="2" | ||
| d="M13 7l5 5m0 0l-5 5m5-5H6" | ||
| /> | ||
| </svg> |
There was a problem hiding this comment.
Mark decorative CTA icons as non-semantic.
The arrow icons are visual-only. Add aria-hidden="true" (and optionally focusable="false") so assistive tech doesn’t announce extra noise.
Also applies to: 50-52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/customer/event/Hero.svelte` around lines 23 - 30, The
decorative arrow SVGs in the Hero.svelte component are currently semantic to
assistive tech; update each arrow <svg> element (the CTA arrow icons) to be
non-semantic by adding aria-hidden="true" and optionally focusable="false" so
screen readers ignore them; apply the same change to the other arrow SVG
instance noted (the one around lines 50-52) to keep accessibility consistent.
|
|
||
| <CustomerFooter /> | ||
| </div> | ||
| 🧭 2️⃣ Navbar. |
There was a problem hiding this comment.
Remove the stray note from the layout body.
Line 25 is plain markup text, so it will render 🧭 2️⃣ Navbar. on every customer page instead of staying as an internal note.
✂️ Minimal fix
-🧭 2️⃣ Navbar.📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| 🧭 2️⃣ Navbar. |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/`(customer)/+layout.svelte at line 25, Remove the stray markup
note "🧭 2️⃣ Navbar." from the layout template in +layout.svelte so it no longer
renders on every customer page; locate the literal text "🧭 2️⃣ Navbar." in the
file and delete it (or move it to a code comment) to keep the note internal
rather than part of the rendered layout.
| const eventList = await db | ||
| .select() | ||
| .from(events) | ||
| .where(and(...conditions)) | ||
| .orderBy(desc(events.createdAt)) // Mới nhất lên đầu | ||
| .limit(limit) | ||
| .offset(offset); | ||
|
|
||
| // 4. Đếm tổng số sự kiện để làm phân trang (Pagination) | ||
| // Tạm thời lấy length để làm demo nhanh, dự án thật sẽ dùng hàm COUNT() của DB | ||
| const totalEvents = await db | ||
| .select({ count: count() }) | ||
| .from(events) | ||
| .where(and(...conditions)) | ||
| .then((result) => result[0]?.count || 0); | ||
|
|
||
| const totalPages = Math.ceil(totalEvents / limit); | ||
| // 5. Trả dữ liệu về cho Frontend (+page.svelte) | ||
| return { | ||
| events: eventList, | ||
| pagination: { | ||
| currentPage: page, | ||
| totalPages, | ||
| searchQuery, | ||
| }, | ||
| }; |
There was a problem hiding this comment.
Clamp out-of-range page values before rendering the empty state.
Lines 24-30 fetch with the raw requested page before the total is known. If ?page is larger than the last page, this returns [], and the customer page will show "no events" even though matching published events still exist.
📄 One way to fix it
- const limit = 8; // Hiển thị 8 sự kiện 1 trang
- const offset = (page - 1) * limit;
+ const limit = 8; // Hiển thị 8 sự kiện 1 trang
// 2. Viết điều kiện lọc (Chỉ lấy sự kiện 'published')
const conditions = [eq(events.status, 'published')];
// Nếu user có gõ tìm kiếm, thêm điều kiện tìm theo tiêu đề (ilike: không phân biệt hoa thường)
if (searchQuery) {
conditions.push(ilike(events.title, `%${escapedQuery}%`));
}
- // 3. Query Database
- const eventList = await db
- .select()
- .from(events)
- .where(and(...conditions))
- .orderBy(desc(events.createdAt)) // Mới nhất lên đầu
- .limit(limit)
- .offset(offset);
-
- // 4. Đếm tổng số sự kiện để làm phân trang (Pagination)
- // Tạm thời lấy length để làm demo nhanh, dự án thật sẽ dùng hàm COUNT() của DB
- const totalEvents = await db
+ // 3. Đếm tổng số sự kiện để làm phân trang (Pagination)
+ const totalEvents = Number(
+ (
+ await db
+ .select({ count: count() })
+ .from(events)
+ .where(and(...conditions))
+ )[0]?.count ?? 0,
+ );
+
+ const totalPages = Math.ceil(totalEvents / limit);
+ const currentPage = totalPages === 0 ? 1 : Math.min(page, totalPages);
+ const offset = (currentPage - 1) * limit;
+
+ // 4. Query Database
+ const eventList = await db
.select({ count: count() })
.from(events)
.where(and(...conditions))
- .then((result) => result[0]?.count || 0);
-
- const totalPages = Math.ceil(totalEvents / limit);
+ .orderBy(desc(events.createdAt))
+ .limit(limit)
+ .offset(offset);
+
// 5. Trả dữ liệu về cho Frontend (+page.svelte)
return {
events: eventList,
pagination: {
- currentPage: page,
+ currentPage,
totalPages,
searchQuery,
},
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/`(customer)/+page.server.ts around lines 24 - 49, Compute
totalEvents and totalPages before querying eventList, then clamp the requested
page (page) into the valid range [1, totalPages] and recompute offset before
running the eventList query; specifically, move the COUNT query using
db.select({ count: count() }).from(events).where(and(...conditions)) ahead of
the eventList query, calculate totalPages = Math.ceil(totalEvents / limit), set
page = Math.max(1, Math.min(page, totalPages || 1)), recalc offset = (page - 1)
* limit, and then run the db.select()
.from(events).where(and(...conditions)).orderBy(desc(events.createdAt)).limit(limit).offset(offset)
to avoid rendering an empty state for out‑of‑range page values.
| interface Event { | ||
| id: number; | ||
| title: string; | ||
| eventDate: string | Date; | ||
| venue: string; | ||
| bannerImageUrl?: string; | ||
| min_price: number | string; | ||
| } |
There was a problem hiding this comment.
The category bar is wired as state only, not as a filter.
Lines 46-47 track activeCategory, but Lines 120-126 still render the raw events array, and the Event type in Lines 10-17 does not expose any category field to filter on. Right now users can switch categories with no change to the cards or empty state.
Also applies to: 46-47, 101-126
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/`(customer)/+page.svelte around lines 10 - 17, The Event type and
rendering need to support category filtering: add a category (e.g., category:
string) to the Event interface and ensure incoming event objects populate it;
create a derived variable (e.g., filteredEvents) that filters the existing
events array by activeCategory (treat a special "All" or empty value as no
filter) and use filteredEvents in the template where events are mapped/rendered
(instead of raw events) and in the empty-state check so switching activeCategory
updates the displayed cards and empty state accordingly.
| .view-all-link { | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: 4px; | ||
| font-size: 0.875rem; | ||
| font-weight: 600; | ||
| color: var(--color-primary-dim); | ||
| transition: gap 0.2s; | ||
| text-decoration: none; | ||
| } |
There was a problem hiding this comment.
Use a defined color token for the "Xem tất cả" link.
Line 177 references --color-primary-dim, but src/routes/(customer)/+layout.svelte Lines 28-39 never define that variable. The link color will fall back unpredictably instead of using the intended theme color.
🎨 Minimal fix
- color: var(--color-primary-dim);
+ color: var(--color-primary);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| .view-all-link { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--color-primary-dim); | |
| transition: gap 0.2s; | |
| text-decoration: none; | |
| } | |
| .view-all-link { | |
| display: inline-flex; | |
| align-items: center; | |
| gap: 4px; | |
| font-size: 0.875rem; | |
| font-weight: 600; | |
| color: var(--color-primary); | |
| transition: gap 0.2s; | |
| text-decoration: none; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/`(customer)/+page.svelte around lines 171 - 180, The
.view-all-link rule uses an undefined CSS variable --color-primary-dim; either
change that usage to a theme token that actually exists (e.g., replace
--color-primary-dim with the existing primary token used in your layout, such as
--color-primary) in the .view-all-link selector, or add a definition for
--color-primary-dim in the layout’s :root/theme variables so the value is
defined globally; update the .view-all-link class (and/or the layout variables)
accordingly to ensure the link uses a defined color token.
| "strict": true, | ||
| "moduleResolution": "bundler" | ||
| "moduleResolution": "bundler", | ||
| "types": ["node", "bun-types"] |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify if Bun globals are currently used in browser/universal code.
# Expected: no Bun globals in component and universal route code.
rg -nP --type=ts --type=svelte -C2 '\bBun\b|\bbun:\w+|\bBunFile\b|\bBun\.'Repository: Tixtac-Project/tixtac
Length of output: 95
🏁 Script executed:
#!/bin/bash
# Check current tsconfig.json
cat -n tsconfig.json
# Search for Bun globals in source files (using file patterns instead of types)
echo "=== Searching for Bun usage ==="
rg -nP '\bBun\b|\bbun:' --glob='*.ts' --glob='*.tsx' --glob='*.js' --glob='*.jsx' --glob='*.svelte' -C1
# Check repository structure
echo "=== Repository structure ==="
ls -la | head -20Repository: Tixtac-Project/tixtac
Length of output: 2419
🏁 Script executed:
# Verify SvelteKit structure and universal/client code
echo "=== Checking src structure ==="
fd -t f '\.(ts|tsx|js|jsx|svelte)$' src/ | head -20
echo "=== Checking routes structure ==="
fd -t d 'routes' | head -10
echo "=== Checking for client-only markers ==="
rg -l 'browser\s*:?\s*true|ssr\s*:\s*false|@sveltejs/adapter' --glob='*.ts' --glob='*.js' --glob='*.svelte' | head -10Repository: Tixtac-Project/tixtac
Length of output: 1174
Remove bun-types from the global TypeScript scope to prevent accidentally using Bun APIs in client code.
Currently, bun-types is included in the global types configuration alongside node, which exposes Bun APIs to all files—including client and universal components—during type-checking. While Bun is only used in test files (src/lib/utils/seat-label.test.ts), exposing these types globally could mask type-checking mistakes if Bun APIs are accidentally imported in browser code.
Scope Bun typings to server-only contexts instead:
Suggested fix
- "types": ["node", "bun-types"]
+ "types": ["node"]Then add a server-only ambient declaration (example):
/// <reference types="bun-types" />in a server-only .d.ts file (e.g., near server runtime entrypoints or in a src/lib/types/bun.d.ts file specifically for test/server code).
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| "types": ["node", "bun-types"] | |
| "types": ["node"] |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@tsconfig.json` at line 14, Remove "bun-types" from the global "types" array
in tsconfig.json so Bun APIs are not exposed to client/universal code, and
instead create a server-only ambient declaration file that adds Bun typings via
a triple-slash directive (e.g., a new .d.ts containing /// <reference
types="bun-types" />) and ensure only server/test compilation picks up that file
(so tests like seat-label.test.ts get Bun types but client code does not);
update any server/test-specific tsconfig or include patterns to reference that
new .d.ts as needed.
Mô tả
Thực hiện xây dựng cấu trúc layout cho trang chủ khách hàng (customer homepage) và tách các giao diện thành các component tái sử dụng. Đồng thời tối ưu hóa cấu trúc component của trang admin và cập nhật service liên quan.
Closes #12
Loại thay đổi
Screenshots / Demo
Checklist
bun run dev)bun run check)bun run lint)bun run format)feat:,fix:,chore:,...)Ghi chú cho reviewer
Summary by CodeRabbit
Release Notes
New Features
Improvements
Dependencies