feat(ui): add ui customer homepage - #62
Conversation
📝 WalkthroughWalkthroughIntroduced a new customer-facing event listing page with layout components (navbar, footer, search bar), event display components (cards, pagination, category filter, empty state), and server-side data loading. Also removed legacy root page, updated dependencies, and adjusted admin event filtering logic. Changes
Sequence Diagram(s)sequenceDiagram
actor User
participant Browser as Browser /<br/>(customer)
participant RouteHandler as Route Handler<br/>(+page.server.ts)
participant Service as Event Service
participant Database as Database
participant Component as Page Component<br/>(+page.svelte)
User->>Browser: Request page (with q & page params)
Browser->>RouteHandler: Load page data
RouteHandler->>RouteHandler: Parse query params (q, page)
RouteHandler->>Service: listEvents(filters, pagination)
Service->>Database: Query published events<br/>(with search & pagination)
Database-->>Service: events[], count
Service-->>RouteHandler: events[], totalPages
RouteHandler-->>Component: { events, pagination }
Component->>Component: Render EventCard grid
Component->>Component: Render CategoryBar filter
Component->>Component: Render Pagination (if totalPages > 1)
Component-->>Browser: HTML (events list)
Browser-->>User: Display events page
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (4 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: 7
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
package.json (1)
60-68:⚠️ Potential issue | 🔴 CriticalAdd
@tanstack/table-coreto dependencies.
src/lib/components/ui/data-table/data-table.svelte.tsimportscreateTablefrom@tanstack/table-coreon line 8, andsrc/lib/components/ui/data-table/flex-render.sveltealso imports types from the same package. Without this dependency, module resolution will fail at runtime.Suggested fix
"dependencies": { + "@tanstack/table-core": "^9.0.0-alpha.32", "amqplib": "^1.0.3", "argon2": "^0.44.0",🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@package.json` around lines 60 - 68, Add `@tanstack/table-core` to package.json dependencies so runtime module resolution succeeds for imports like createTable and related types; update dependencies to include "@tanstack/table-core" with an appropriate version (matching other frontend libs) and run your package manager install to ensure imports in src/lib/components/ui/data-table/data-table.svelte.ts (createTable) and src/lib/components/ui/data-table/flex-render.svelte (table-related types) resolve correctly.
🧹 Nitpick comments (4)
src/lib/components/customer/event/Pagination.svelte (1)
4-9: Remove unused requiredsearchQueryfrom the component contract.On Line 7,
searchQueryis required in props typing but never read. This makes the API misleading and forces callers to pass a no-op prop.Suggested fix
let { currentPage, totalPages, buildPageUrl } = $props<{ currentPage: number; totalPages: number; - searchQuery: string; buildPageUrl: (page: number) => string; }>();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/components/customer/event/Pagination.svelte` around lines 4 - 9, The props typing for the Pagination component incorrectly includes a required searchQuery that is never used; remove searchQuery from the props generic and any related mentions so the destructuring stays as let { currentPage, totalPages, buildPageUrl } = $props<{ currentPage: number; totalPages: number; buildPageUrl: (page: number) => string; }>(); — update the component's prop contract in Pagination.svelte to reflect only currentPage, totalPages and buildPageUrl.src/lib/components/customer/layout/CustomerFooter.svelte (1)
86-102: Placeholder internal links point to non-existent routes.Links like
/about,/careers,/faq,/terms, etc. likely don't have corresponding routes yet. Consider either:
- Adding
TODOcomments to track these- Using
#href witharia-disableduntil routes exist- Keeping as-is if routes are planned for near-term implementation
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/components/customer/layout/CustomerFooter.svelte` around lines 86 - 102, The footer contains internal anchor links created via resolve('/about'), resolve('/careers'), resolve('/faq'), resolve('/terms'), resolve('/privacy'), etc., which point to routes that don't yet exist; update CustomerFooter.svelte to either mark these as TODOs or disable them until routes are implemented: for each unresolved link (identify by ids like footer-about, footer-careers, footer-faq, footer-terms, footer-privacy) replace the href with a safe placeholder (e.g., '#' or remove navigation) and add aria-disabled and a TODO comment referencing the intended route name, or alternatively add the pending routes in the router if they are ready—ensure accessibility attributes and link ids remain intact.src/lib/components/customer/layout/CustomerNavbar.svelte (1)
6-6: Consider adding a type annotation for theuserprop.The
userprop lacks explicit typing. Adding a type improves maintainability and IDE support.✨ Suggested type annotation
- let { user } = $props(); + let { user } = $props<{ + user: { id: number; role: 'admin' | 'customer' } | null; + }>();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/lib/components/customer/layout/CustomerNavbar.svelte` at line 6, The prop declaration "let { user } = $props();" lacks an explicit type; update the CustomerNavbar component to annotate the user prop (e.g., change the destructured prop to include a type like "{ user: User }" or another existing app user type) and ensure that the referenced type is imported or defined (create an interface/type alias such as User if none exists) so the line becomes a typed destructure from $props and IDE/type-checking will recognize user’s shape.src/routes/(customer)/+page.server.ts (1)
6-49: Consider reusingeventService.listEvents()instead of duplicating query logic.This file duplicates pagination, filtering, and search logic that already exists in
eventService.listEvents(). The service version also includes seat aggregation (min_price,totalSeats,availableSeats) which this implementation lacks.Key differences:
- This orders by
createdAt desc; service orders byeventDate- This returns raw event data; service returns formatted data with
min_price- Search escaping is handled here but not in the service (potential inconsistency)
♻️ Suggested refactor using the existing service
-import { db } from '$lib/server/db'; -import { events } from '$lib/server/db/schema'; -import { and, count, desc, eq, ilike } from 'drizzle-orm'; +import { eventService } from '$lib/server/services/event.service'; import type { PageServerLoad } from './$types'; export const load: PageServerLoad = async ({ url }) => { - // 1. Nhận từ khóa tìm kiếm và trang hiện tại từ URL (VD: ?q=rock&page=1) const searchQuery = url.searchParams.get('q') || ''; - const escapedQuery = searchQuery.replace(/[%_]/g, '\\$&'); - const rawPage = Number.parseInt(url.searchParams.get('page') ?? '1', 10); - const page = Number.isFinite(rawPage) && rawPage > 0 ? rawPage : 1; - const limit = 8; // Hiển thị 8 sự kiện 1 trang - const offset = (page - 1) * limit; - - // 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) - const totalEvents = await db - .select({ count: count() }) - .from(events) - .where(and(...conditions)) - .then((result) => result[0]?.count || 0); + const page = url.searchParams.get('page') ?? '1'; - const totalPages = Math.ceil(totalEvents / limit); - // 5. Trả dữ liệu về cho Frontend (+page.svelte) + const result = await eventService.listEvents({ + q: searchQuery || undefined, + page, + limit: 8, + // No role/userId = only published events + }); + return { - events: eventList, + events: result.events, pagination: { - currentPage: page, - totalPages, + currentPage: result.pagination.page, + totalPages: result.pagination.total_pages, searchQuery, }, }; };Note: If you keep the current implementation, consider adding the
%/_escaping toeventService.listEvents()as well for consistency.🤖 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 6 - 49, The page-level load function duplicates querying/pagination/search logic; replace the custom DB calls in export const load with a call to eventService.listEvents(...) passing normalized inputs (escaped searchQuery, page, limit) so you reuse seat aggregation (min_price, totalSeats, availableSeats) and formatted ordering from eventService.listEvents; ensure you propagate ordering semantics (decide to use eventService's eventDate order or keep createdAt and update service if needed), and return the same shape { events, pagination: { currentPage: page, totalPages, searchQuery } } expected by the frontend; if you keep DB logic here instead, move the %/_ escaping into eventService.listEvents to avoid inconsistency.
🤖 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/customer/event/EmptyState.svelte`:
- Around line 9-13: The component currently hardcodes the empty-state title,
subtitle and action ("Không tìm thấy..." and "Xóa tìm kiếm") — change the render
logic in EmptyState.svelte to branch on whether there's an active search/query
and show two variants: when there is an active query, render the existing "Không
tìm thấy sự kiện nào!" title, the search-miss subtitle, and the Button with
href={clearSearchUrl} (id="events-clear-search"); when there is no active query
render the default empty state title "Chưa có sự kiện nào" (and a
neutral/helpful subtitle or no action). Locate the elements with class names
empty-title / empty-sub and the Button component to implement the conditional
rendering based on the query prop/store.
In `@src/lib/components/customer/event/EventCard.svelte`:
- Around line 46-49: The date badge currently constructs Date objects directly
from event.eventDate in the date-day and date-month spans, which causes timezone
drift and duplicates parsing logic; update the badge to reuse the existing
formatDate helper (or, if you must parse manually, construct a local-midnight
Date via parsed year/month/day) so both the day and month use a single,
timezone-safe parsed value (reference event.eventDate, the date-day/date-month
spans, and formatDate) and remove the duplicated new Date(...) calls.
In `@src/lib/components/customer/layout/AuthNav.svelte`:
- Around line 23-25: The form currently uses SvelteKit's unsupported use:enhance
with an API `+server` endpoint; remove use:enhance from the <form
action="/api/auth/logout"> and change the client flow to a manual fetch in your
existing handleLogout function (or attach handleLogout to on:submit/on:click),
where you call fetch('/api/auth/logout', { method: 'POST', credentials:
'same-origin' }), await the response, handle non-2xx errors and success
(redirect or update UI), and preventDefault() on the event; update the
form/button wiring so handleLogout is invoked directly instead of relying on
use:enhance.
In `@src/lib/components/customer/layout/CustomerSearchBar.svelte`:
- Around line 9-11: The SVG in CustomerSearchBar.svelte (the <svg
class="search-icon"> with its <path>) lacks stroke/fill attributes so the path
won't render; update the <svg> and/or <path> for the search-icon to include
rendering attributes (e.g., set stroke="currentColor", fill="none" and an
appropriate stroke-width) so the icon displays and inherits text color, ensuring
accessibility attributes (aria-hidden or role) remain correct.
In `@src/routes/`(customer)/+layout.svelte:
- Line 25: Remove the stray debug/placeholder text node "🧭 2️⃣ Navbar." from
the Svelte layout template so it doesn't render between the main content and
footer; locate the plain text entry in the +layout.svelte markup (the text node
that sits between the main content block and the footer component) and delete it
(or replace it with a proper comment or component invocation if needed) to
prevent it from appearing in the UI.
In `@src/routes/`(customer)/+page.svelte:
- Around line 10-17: The Event interface in +page.svelte declares min_price but
the server page loader (+page.server.ts) returns raw events without min_price,
so formatPrice always falls back to "Đang cập nhật"; fix by changing the
server-side loader to return events from eventService.listEvents() (or otherwise
include the seat_sections aggregation that computes min_price) so each event has
min_price populated, and keep the Event interface and formatPrice usage as-is;
ensure the returned objects' min_price type matches the interface
(number|string) and update any mapping code that transforms events before
sending to the page.
- Line 46: activeCategory is bound to CategoryBar but never applied to the
events list, so implement client-side filtering by deriving a filteredEvents
array from events using activeCategory (e.g., a reactive statement or derived
store) and render filteredEvents instead of events; ensure you reference the
state variable activeCategory, the events array, and the CategoryBar binding,
and guard for an empty/'' activeCategory to return all events (also confirm
events include a category property), or if you intend server-side filtering
remove the activeCategory binding and CategoryBar usage to avoid a no-op
control.
---
Outside diff comments:
In `@package.json`:
- Around line 60-68: Add `@tanstack/table-core` to package.json dependencies so
runtime module resolution succeeds for imports like createTable and related
types; update dependencies to include "@tanstack/table-core" with an appropriate
version (matching other frontend libs) and run your package manager install to
ensure imports in src/lib/components/ui/data-table/data-table.svelte.ts
(createTable) and src/lib/components/ui/data-table/flex-render.svelte
(table-related types) resolve correctly.
---
Nitpick comments:
In `@src/lib/components/customer/event/Pagination.svelte`:
- Around line 4-9: The props typing for the Pagination component incorrectly
includes a required searchQuery that is never used; remove searchQuery from the
props generic and any related mentions so the destructuring stays as let {
currentPage, totalPages, buildPageUrl } = $props<{ currentPage: number;
totalPages: number; buildPageUrl: (page: number) => string; }>(); — update the
component's prop contract in Pagination.svelte to reflect only currentPage,
totalPages and buildPageUrl.
In `@src/lib/components/customer/layout/CustomerFooter.svelte`:
- Around line 86-102: The footer contains internal anchor links created via
resolve('/about'), resolve('/careers'), resolve('/faq'), resolve('/terms'),
resolve('/privacy'), etc., which point to routes that don't yet exist; update
CustomerFooter.svelte to either mark these as TODOs or disable them until routes
are implemented: for each unresolved link (identify by ids like footer-about,
footer-careers, footer-faq, footer-terms, footer-privacy) replace the href with
a safe placeholder (e.g., '#' or remove navigation) and add aria-disabled and a
TODO comment referencing the intended route name, or alternatively add the
pending routes in the router if they are ready—ensure accessibility attributes
and link ids remain intact.
In `@src/lib/components/customer/layout/CustomerNavbar.svelte`:
- Line 6: The prop declaration "let { user } = $props();" lacks an explicit
type; update the CustomerNavbar component to annotate the user prop (e.g.,
change the destructured prop to include a type like "{ user: User }" or another
existing app user type) and ensure that the referenced type is imported or
defined (create an interface/type alias such as User if none exists) so the line
becomes a typed destructure from $props and IDE/type-checking will recognize
user’s shape.
In `@src/routes/`(customer)/+page.server.ts:
- Around line 6-49: The page-level load function duplicates
querying/pagination/search logic; replace the custom DB calls in export const
load with a call to eventService.listEvents(...) passing normalized inputs
(escaped searchQuery, page, limit) so you reuse seat aggregation (min_price,
totalSeats, availableSeats) and formatted ordering from eventService.listEvents;
ensure you propagate ordering semantics (decide to use eventService's eventDate
order or keep createdAt and update service if needed), and return the same shape
{ events, pagination: { currentPage: page, totalPages, searchQuery } } expected
by the frontend; if you keep DB logic here instead, move the %/_ escaping into
eventService.listEvents to avoid inconsistency.
🪄 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: 56ec8c78-e2ac-4672-bf19-7a369e6b0997
⛔ Files ignored due to path filters (1)
bun.lockis excluded by!**/*.lock
📒 Files selected for processing (19)
.gitignorepackage.jsonsrc/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/services/event.service.tssrc/routes/(customer)/+layout.sveltesrc/routes/(customer)/+page.server.tssrc/routes/(customer)/+page.sveltesrc/routes/+page.server.tssrc/routes/+page.sveltetsconfig.json
💤 Files with no reviewable changes (2)
- src/routes/+page.svelte
- src/routes/+page.server.ts
| <h3 class="empty-title">Không tìm thấy sự kiện nào!</h3> | ||
| <p class="empty-sub">Hãy thử tìm kiếm với từ khóa khác hoặc khám phá tất cả sự kiện.</p> | ||
| <Button href={clearSearchUrl} size="lg" class="rounded-full" id="events-clear-search"> | ||
| Xóa tìm kiếm | ||
| </Button> |
There was a problem hiding this comment.
Empty-state text/action is hardcoded to “search miss” only.
On Line 9–Line 13, this always shows “Không tìm thấy...” and “Xóa tìm kiếm”. That can violate the default empty-state requirement ("Chưa có sự kiện nào") when there is no active query.
Suggested fix
<script lang="ts">
import { Button } from '$lib/components/ui/button';
- let { clearSearchUrl } = $props<{ clearSearchUrl: string }>();
+ let { clearSearchUrl, hasSearchQuery = false } = $props<{
+ clearSearchUrl: string;
+ hasSearchQuery?: boolean;
+ }>();
</script>
<div class="empty-state">
<div class="empty-icon">🎭</div>
- <h3 class="empty-title">Không tìm thấy sự kiện nào!</h3>
- <p class="empty-sub">Hãy thử tìm kiếm với từ khóa khác hoặc khám phá tất cả sự kiện.</p>
- <Button href={clearSearchUrl} size="lg" class="rounded-full" id="events-clear-search">
- Xóa tìm kiếm
- </Button>
+ {`#if` hasSearchQuery}
+ <h3 class="empty-title">Không tìm thấy sự kiện nào!</h3>
+ <p class="empty-sub">Hãy thử tìm kiếm với từ khóa khác hoặc khám phá tất cả sự kiện.</p>
+ <Button href={clearSearchUrl} size="lg" class="rounded-full" id="events-clear-search">
+ Xóa tìm kiếm
+ </Button>
+ {:else}
+ <h3 class="empty-title">Chưa có sự kiện nào</h3>
+ <p class="empty-sub">Hiện chưa có sự kiện được đăng tải.</p>
+ {/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.
| <h3 class="empty-title">Không tìm thấy sự kiện nào!</h3> | |
| <p class="empty-sub">Hãy thử tìm kiếm với từ khóa khác hoặc khám phá tất cả sự kiện.</p> | |
| <Button href={clearSearchUrl} size="lg" class="rounded-full" id="events-clear-search"> | |
| Xóa tìm kiếm | |
| </Button> | |
| <script lang="ts"> | |
| import { Button } from '$lib/components/ui/button'; | |
| let { clearSearchUrl, hasSearchQuery = false } = $props<{ | |
| clearSearchUrl: string; | |
| hasSearchQuery?: boolean; | |
| }>(); | |
| </script> | |
| <div class="empty-state"> | |
| <div class="empty-icon">🎭</div> | |
| {`#if` hasSearchQuery} | |
| <h3 class="empty-title">Không tìm thấy sự kiện nào!</h3> | |
| <p class="empty-sub">Hãy thử tìm kiếm với từ khóa khác hoặc khám phá tất cả sự kiện.</p> | |
| <Button href={clearSearchUrl} size="lg" class="rounded-full" id="events-clear-search"> | |
| Xóa tìm kiếm | |
| </Button> | |
| {:else} | |
| <h3 class="empty-title">Chưa có sự kiện nào</h3> | |
| <p class="empty-sub">Hiện chưa có sự kiện được đăng tải.</p> | |
| {/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/EmptyState.svelte` around lines 9 - 13, The
component currently hardcodes the empty-state title, subtitle and action ("Không
tìm thấy..." and "Xóa tìm kiếm") — change the render logic in EmptyState.svelte
to branch on whether there's an active search/query and show two variants: when
there is an active query, render the existing "Không tìm thấy sự kiện nào!"
title, the search-miss subtitle, and the Button with href={clearSearchUrl}
(id="events-clear-search"); when there is no active query render the default
empty state title "Chưa có sự kiện nào" (and a neutral/helpful subtitle or no
action). Locate the elements with class names empty-title / empty-sub and the
Button component to implement the conditional rendering based on the query
prop/store.
| <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.
🧩 Analysis chain
🌐 Web query:
How does JavaScript parse date-only strings like "2026-07-21" in new Date() across timezones, and what is the recommended way to render stable local dates in Svelte apps?
💡 Result:
JavaScript's new Date("2026-07-21") parses date-only ISO strings (YYYY-MM-DD) as midnight UTC, then converts to the local timezone when displaying or using local methods. This causes the date to shift to the previous day in timezones west of UTC (e.g., in UTC-5, "2026-07-21" becomes July 20, 19:00 local). This behavior is spec-required for date-only forms without timezone offset, treating them as UTC unlike datetime forms without offset which are local. For stable local dates in Svelte apps, avoid new Date(string) for parsing date-only strings. Instead: Parse to local midnight using new Date(year, month-1, day) or manual split/parse. Store/send as ISO UTC string or timestamp. Render using Intl.DateTimeFormat for locale-aware, stable formatting that respects user's timezone/locale without parsing issues, ensuring SSR/client consistency by passing locale from Accept-Language header via SvelteKit hooks/load.
Citations:
- 1: https://dev.to/musatov/the-subtle-trap-of-iso-date-strings-in-javascript-49co
- 2: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/Date
- 3: https://musatov.com/posts/iso-date-string-parsing/
- 4: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/parse
- 5: https://stackoverflow.com/questions/2587345/why-does-date-parse-give-incorrect-results
- 6: https://stackoverflow.com/questions/68471133/new-date-returning-different-values-for-time-zones-with-different-inputs
- 7: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date
- 8: https://dbushell.com/2026/03/11/sveltekit-internationalization-flash-of-wrong-locale/
- 9: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
- 10: https://www.captaincodeman.com/internationalization-formatting-with-intl-ssr-sveltekit
🏁 Script executed:
cat -n src/lib/components/customer/event/EventCard.svelteRepository: Tixtac-Project/tixtac
Length of output: 8214
🏁 Script executed:
rg -n "formatDate" --type ts --type js --type svelte -B 2 -A 5 | head -100Repository: Tixtac-Project/tixtac
Length of output: 95
🏁 Script executed:
rg -n "formatDate" -B 2 -A 5 | head -150Repository: Tixtac-Project/tixtac
Length of output: 6184
Refactor date-badge rendering to use consistent, timezone-safe date parsing.
Lines 46 and 48 create Date objects directly from event.eventDate without normalizing timezone handling. new Date() parses date-only ISO strings (YYYY-MM-DD) as UTC midnight, causing the date to shift to the previous day in timezones west of UTC (e.g., UTC-5). Additionally, this duplicates date parsing logic instead of using the available formatDate function.
Use a consistent date parsing strategy:
- Reuse formatDate for the badge display, or
- Parse to local midnight explicitly:
new Date(year, month-1, day)using parsed date components - Or use Intl.DateTimeFormat for locale-aware formatting without parsing issues
This approach avoids timezone drift and keeps date handling centralized.
🤖 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 46 - 49, The
date badge currently constructs Date objects directly from event.eventDate in
the date-day and date-month spans, which causes timezone drift and duplicates
parsing logic; update the badge to reuse the existing formatDate helper (or, if
you must parse manually, construct a local-midnight Date via parsed
year/month/day) so both the day and month use a single, timezone-safe parsed
value (reference event.eventDate, the date-day/date-month spans, and formatDate)
and remove the duplicated new Date(...) calls.
| <form action="/api/auth/logout" method="POST" use:enhance={handleLogout}> | ||
| <Button type="submit" variant="destructive" size="sm">Đăng xuất</Button> | ||
| </form> |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify the current mismatch: enhanced form posting to +server endpoint
rg -n "use:enhance|action=\"/api/auth/logout\"" src/lib/components/customer/layout/AuthNav.svelte
fd -i "+server.ts" src/routes/api/auth/logout
fd -i "+page.server.ts" src/routes/api/auth/logoutRepository: Tixtac-Project/tixtac
Length of output: 747
🏁 Script executed:
fd --glob "*/logout/*" src/routes/api/auth
fd --glob "*logout*" src/routes
cat -n src/routes/api/auth/logout/+server.ts 2>/dev/null || cat -n src/routes/api/auth/logout/+server.js 2>/dev/null || echo "File not found at expected locations"Repository: Tixtac-Project/tixtac
Length of output: 418
🌐 Web query:
SvelteKit use:enhance +server endpoint support compatibility
💡 Result:
SvelteKit's use:enhance from $app/forms is designed for progressive enhancement of forms that POST to SvelteKit form actions in +page.server.js (or +layout.server.js). It is not natively compatible with +server.js endpoints. Key points: - Official docs explicitly state: "use:enhance can only be used with forms that have method="POST" and point to actions defined in a +page.server.js file. It will not work with ... posting to a +server.js endpoint will result in an error." {7,11} - When both +page.server.js and +server.js exist, use:enhance targets +server.js by default due to JSON Accept header; use x-sveltekit-action: true header for actions. {1} - Recent attempts to add dev errors for misuse with +server.js were merged then reverted due to breakage with libraries like Superforms that provide compatibility helpers. {3,14} - As of 2026 (latest releases 2.56.0), no native support added; docs recommend against it, issues remain open. {2,7,21,25} - Libraries like sveltekit-superforms offer enhance wrappers that make +server.js endpoints compatible by formatting responses like ActionResults. {12,14}
Citations:
- 1:
use:enhancealways targeting+server.{js|ts}instead of+page.server.{js|ts}sveltejs/kit#6983 - 2:
use:enhancecompatibility on form whose action isn't a SvelteKit action sveltejs/kit#10855 - 3: chore: error during development when using
use:enhancewith+serversveltejs/kit#13197 - 4: https://svelte.dev/docs/kit/form-actions
- 5: https://svelte.dev/docs/kit/$app-forms
- 6: chore: revert dev error when using
use:enhancewith+serversveltejs/kit#13397 - 7: https://github.com/sveltejs/kit/blob/HEAD/packages/kit/CHANGELOG.md
use:enhance is not supported for +server endpoints.
On Line 23, the form posts to /api/auth/logout, which is a +server.ts endpoint. SvelteKit's use:enhance directive is designed exclusively for form actions in +page.server.ts files. Using it with +server endpoints is unsupported and can cause runtime errors or unexpected behavior in logout handling.
Use a manual fetch handler instead:
Suggested fix (manual submit handler for API route)
<script lang="ts">
- import { enhance } from '$app/forms';
import { goto, invalidateAll } from '$app/navigation';
import { resolve } from '$app/paths';
import { Button } from '$lib/components/ui/button';
@@
- function handleLogout() {
- return async () => {
+ async function handleLogout(event: SubmitEvent) {
+ event.preventDefault();
+ const form = event.currentTarget as HTMLFormElement;
+ const res = await fetch(form.action, { method: 'POST' });
+ if (!res.ok) return;
await invalidateAll();
await goto(resolve('/'));
- };
}
</script>
@@
- <form action="/api/auth/logout" method="POST" use:enhance={handleLogout}>
+ <form action="/api/auth/logout" method="POST" onsubmit={handleLogout}>
<Button type="submit" variant="destructive" size="sm">Đăng xuất</Button>
</form>📝 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.
| <form action="/api/auth/logout" method="POST" use:enhance={handleLogout}> | |
| <Button type="submit" variant="destructive" size="sm">Đăng xuất</Button> | |
| </form> | |
| <script lang="ts"> | |
| import { goto, invalidateAll } from '$app/navigation'; | |
| import { resolve } from '$app/paths'; | |
| import { Button } from '$lib/components/ui/button'; | |
| async function handleLogout(event: SubmitEvent) { | |
| event.preventDefault(); | |
| const form = event.currentTarget as HTMLFormElement; | |
| const res = await fetch(form.action, { method: 'POST' }); | |
| if (!res.ok) return; | |
| await invalidateAll(); | |
| await goto(resolve('/')); | |
| } | |
| </script> | |
| <form action="/api/auth/logout" method="POST" onsubmit={handleLogout}> | |
| <Button type="submit" variant="destructive" size="sm">Đăng xuất</Button> | |
| </form> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/customer/layout/AuthNav.svelte` around lines 23 - 25, The
form currently uses SvelteKit's unsupported use:enhance with an API `+server`
endpoint; remove use:enhance from the <form action="/api/auth/logout"> and
change the client flow to a manual fetch in your existing handleLogout function
(or attach handleLogout to on:submit/on:click), where you call
fetch('/api/auth/logout', { method: 'POST', credentials: 'same-origin' }), await
the response, handle non-2xx errors and success (redirect or update UI), and
preventDefault() on the event; update the form/button wiring so handleLogout is
invoked directly instead of relying on use:enhance.
| <svg class="search-icon" viewBox="0 0 24 24"> | ||
| <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | ||
| </svg> |
There was a problem hiding this comment.
Search icon SVG missing stroke/fill attributes — icon won't render.
The SVG path uses stroke-based drawing but lacks stroke and fill attributes. The icon will be invisible.
🐛 Proposed fix
- <svg class="search-icon" viewBox="0 0 24 24">
+ <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>📝 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.
| <svg class="search-icon" viewBox="0 0 24 24"> | |
| <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
| </svg> | |
| <svg class="search-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"> | |
| <path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /> | |
| </svg> |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/lib/components/customer/layout/CustomerSearchBar.svelte` around lines 9 -
11, The SVG in CustomerSearchBar.svelte (the <svg class="search-icon"> with its
<path>) lacks stroke/fill attributes so the path won't render; update the <svg>
and/or <path> for the search-icon to include rendering attributes (e.g., set
stroke="currentColor", fill="none" and an appropriate stroke-width) so the icon
displays and inherits text color, ensuring accessibility attributes (aria-hidden
or role) remain correct.
|
|
||
| <CustomerFooter /> | ||
| </div> | ||
| 🧭 2️⃣ Navbar. |
There was a problem hiding this comment.
Remove stray debug text that will render on the page.
The text "🧭 2️⃣ Navbar." at line 25 appears to be leftover debug/placeholder content. It will be visible to users between the main content and footer.
🐛 Proposed fix
<CustomerFooter />
</div>
-🧭 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. | |
| <CustomerFooter /> | |
| </div> |
🤖 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
debug/placeholder text node "🧭 2️⃣ Navbar." from the Svelte layout template so
it doesn't render between the main content and footer; locate the plain text
entry in the +layout.svelte markup (the text node that sits between the main
content block and the footer component) and delete it (or replace it with a
proper comment or component invocation if needed) to prevent it from appearing
in the UI.
| interface Event { | ||
| id: number; | ||
| title: string; | ||
| eventDate: string | Date; | ||
| venue: string; | ||
| bannerImageUrl?: string; | ||
| min_price: number | string; | ||
| } |
There was a problem hiding this comment.
min_price will always be undefined — prices won't display.
The local Event interface expects min_price, but +page.server.ts returns raw event data without this field (it doesn't join seat_sections). The formatPrice function gracefully falls back to "Đang cập nhật" for all events.
This ties into the earlier suggestion to use eventService.listEvents(), which includes the min_price aggregation.
Also applies to: 59-67
🤖 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 interface
in +page.svelte declares min_price but the server page loader (+page.server.ts)
returns raw events without min_price, so formatPrice always falls back to "Đang
cập nhật"; fix by changing the server-side loader to return events from
eventService.listEvents() (or otherwise include the seat_sections aggregation
that computes min_price) so each event has min_price populated, and keep the
Event interface and formatPrice usage as-is; ensure the returned objects'
min_price type matches the interface (number|string) and update any mapping code
that transforms events before sending to the page.
| { label: 'Comedy', icon: '😂', value: 'comedy' }, | ||
| ]; | ||
|
|
||
| let activeCategory = $state(''); |
There was a problem hiding this comment.
Category filter UI is wired but doesn't filter events.
activeCategory is bound to CategoryBar but never used to filter the events array. Users can select categories, but the displayed events won't change.
Either implement client-side filtering or remove the binding if server-side filtering is planned.
💡 Example client-side filtering implementation
+ // Filter events by category (client-side)
+ let filteredEvents = $derived(
+ activeCategory
+ ? events.filter((e) => e.category === activeCategory)
+ : events
+ );
+
// In template, use filteredEvents instead of events:
- {`#each` events as event, i (event.id)}
+ {`#each` filteredEvents as event, i (event.id)}Note: This requires events to have a category field from the server.
Also applies to: 101-101
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/routes/`(customer)/+page.svelte at line 46, activeCategory is bound to
CategoryBar but never applied to the events list, so implement client-side
filtering by deriving a filteredEvents array from events using activeCategory
(e.g., a reactive statement or derived store) and render filteredEvents instead
of events; ensure you reference the state variable activeCategory, the events
array, and the CategoryBar binding, and guard for an empty/'' activeCategory to
return all events (also confirm events include a category property), or if you
intend server-side filtering remove the activeCategory binding and CategoryBar
usage to avoid a no-op control.
|
❌Chưa có loading state khi fetch |
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
New Features
Chores