Skip to content
This repository was archived by the owner on May 19, 2026. It is now read-only.

feat(ui): add ui customer homepage - #62

Closed
NguyenVanThanhTung wants to merge 4 commits into
mainfrom
feat/12-customer-homepage-ui
Closed

feat(ui): add ui customer homepage#62
NguyenVanThanhTung wants to merge 4 commits into
mainfrom
feat/12-customer-homepage-ui

Conversation

@NguyenVanThanhTung

@NguyenVanThanhTung NguyenVanThanhTung commented Apr 11, 2026

Copy link
Copy Markdown
Contributor

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

  • ✨ Feature mới
  • 🐛 Bug fix
  • ♻️ Refactor
  • 💄 UI / Style
  • 📝 Documentation
  • 🔧 Chore (config, dependencies,...)

Screenshots / Demo

Screenshot (12) Screenshot (13) Screenshot (14) Screenshot 2026-04-11 233015

Checklist

  • Code chạy không lỗi (bun run dev)
  • TypeScript check pass (bun run check)
  • Lint pass (bun run lint)
  • Đã format code (bun run format)
  • Đã test thủ công chức năng
  • Commit message đúng convention (feat:, fix:, chore:,...)

Ghi chú cho reviewer

  • Lưu ý: Layout của phần customer đã được tách biệt hoàn toàn qua file src/routes/(customer)/+layout.svelte.

Summary by CodeRabbit

  • New Features

    • Added events discovery landing page with hero section and marketing content
    • Implemented events search functionality to find events by title
    • Added category filtering to browse events by type
    • Introduced pagination for browsing large event listings
    • Enhanced site navigation with improved header, logo, and footer
    • Added auth-aware navigation showing login/register for guests and tickets link for logged-in users
    • Improved admin access to view both published and draft events
  • Chores

    • Updated build tooling dependencies

@coderabbitai

coderabbitai Bot commented Apr 11, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Introduced 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

Cohort / File(s) Summary
Customer Layout Components
src/lib/components/customer/layout/Logo.svelte, src/lib/components/customer/layout/CustomerNavbar.svelte, src/lib/components/customer/layout/AuthNav.svelte, src/lib/components/customer/layout/CustomerSearchBar.svelte, src/lib/components/customer/layout/CustomerFooter.svelte
New layout components for navbar (logo, search, auth state), footer with social/app store links, and search bar with query parameter binding.
Customer Event Page Components
src/lib/components/customer/event/EventCard.svelte, src/lib/components/customer/event/Hero.svelte, src/lib/components/customer/event/CategoryBar.svelte, src/lib/components/customer/event/Pagination.svelte, src/lib/components/customer/event/EmptyState.svelte
New event listing UI components including card renderer (with image/gradient, badges, pricing), hero section, horizontal category filter, pagination controls, and empty-state fallback.
Customer Routes
src/routes/(customer)/+layout.svelte, src/routes/(customer)/+page.svelte, src/routes/(customer)/+page.server.ts
New customer route group with layout (navbar/footer wrapper), events listing page (grid with search/filter/pagination), and server loader (fetches published events with search/pagination support).
Event Service
src/lib/server/services/event.service.ts
Adjusted listEvents filtering to allow admins to see both published events and their own draft events when userId is provided.
Root Routes
src/routes/+page.server.ts, src/routes/+page.svelte
Removed legacy root page and its server loader (infrastructure health checks deleted).
Configuration & Dependencies
.gitignore, package.json, tsconfig.json
Added /.skill to git ignore, added bun-types devDependency, removed @tanstack/table-core dependency, and updated TypeScript types to include ["node", "bun-types"].

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
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • PR #46: Modifies event.service.ts to adjust event filtering logic in the same listEvents method affected by this change.
  • PR #53: Updates event.service.ts filtering and modifies package.json dependencies for type definitions and table components.
  • PR #45: Extends event.service.ts with create/update/publish operations affecting the same service layer modified here.

Suggested reviewers

  • DungxND
  • HungND-flocus

Poem

🐰 Hopping through pages, I've spun up the view,
Events in grids, with categories too!
Search and paginate, filters so neat,
Customer landing—now that's pretty sweet! 🎫✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Out of Scope Changes check ❓ Inconclusive Minor out-of-scope changes detected: tsconfig.json types configuration and service logic adjustment for admin draft event filtering are not directly required by issue #12. Consider whether tsconfig.json types and admin event service adjustments are necessary dependencies for the customer homepage feature, or should be separated into a different PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The PR successfully implements all core requirements from issue #12: customer layout with navbar/footer, event listing page with search and pagination, server-side event loading with filters, responsive grid design, and empty state handling.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description check ✅ Passed PR description provides a clear summary, specifies correct change type, references the closed issue, includes screenshots, and has a complete verification checklist.
Title check ✅ Passed The title 'feat(ui): add ui customer homepage' accurately summarizes the main change: adding UI components for the customer homepage feature.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/12-customer-homepage-ui

Comment @coderabbitai help to get the list of available commands and usage tips.

@HungND-flocus HungND-flocus changed the title Feat/12 customer homepage UI feat(ui): add ui customer homepage Apr 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 | 🔴 Critical

Add @tanstack/table-core to dependencies.

src/lib/components/ui/data-table/data-table.svelte.ts imports createTable from @tanstack/table-core on line 8, and src/lib/components/ui/data-table/flex-render.svelte also 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 required searchQuery from the component contract.

On Line 7, searchQuery is 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 TODO comments to track these
  • Using # href with aria-disabled until 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 the user prop.

The user prop 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 reusing eventService.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 by eventDate
  • 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 to eventService.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

📥 Commits

Reviewing files that changed from the base of the PR and between 3fd66d4 and c7ee8f9.

⛔ Files ignored due to path filters (1)
  • bun.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • .gitignore
  • package.json
  • src/lib/components/customer/event/CategoryBar.svelte
  • src/lib/components/customer/event/EmptyState.svelte
  • src/lib/components/customer/event/EventCard.svelte
  • src/lib/components/customer/event/Hero.svelte
  • src/lib/components/customer/event/Pagination.svelte
  • src/lib/components/customer/layout/AuthNav.svelte
  • src/lib/components/customer/layout/CustomerFooter.svelte
  • src/lib/components/customer/layout/CustomerNavbar.svelte
  • src/lib/components/customer/layout/CustomerSearchBar.svelte
  • src/lib/components/customer/layout/Logo.svelte
  • src/lib/server/services/event.service.ts
  • src/routes/(customer)/+layout.svelte
  • src/routes/(customer)/+page.server.ts
  • src/routes/(customer)/+page.svelte
  • src/routes/+page.server.ts
  • src/routes/+page.svelte
  • tsconfig.json
💤 Files with no reviewable changes (2)
  • src/routes/+page.svelte
  • src/routes/+page.server.ts

Comment on lines +9 to +13
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

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.

Suggested change
<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.

Comment on lines +46 to +49
<span class="date-day">{new Date(event.eventDate).getDate()}</span>
<span class="date-month">
{new Date(event.eventDate).toLocaleString('vi-VN', { month: 'short' })}
</span>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

🧩 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:


🏁 Script executed:

cat -n src/lib/components/customer/event/EventCard.svelte

Repository: Tixtac-Project/tixtac

Length of output: 8214


🏁 Script executed:

rg -n "formatDate" --type ts --type js --type svelte -B 2 -A 5 | head -100

Repository: Tixtac-Project/tixtac

Length of output: 95


🏁 Script executed:

rg -n "formatDate" -B 2 -A 5 | head -150

Repository: 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:

  1. Reuse formatDate for the badge display, or
  2. Parse to local midnight explicitly: new Date(year, month-1, day) using parsed date components
  3. 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.

Comment on lines +23 to +25
<form action="/api/auth/logout" method="POST" use:enhance={handleLogout}>
<Button type="submit" variant="destructive" size="sm">Đăng xuất</Button>
</form>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 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/logout

Repository: 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:


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.

Suggested change
<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.

Comment on lines +9 to +11
<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>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
<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.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
🧭 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.

Comment on lines +10 to +17
interface Event {
id: number;
title: string;
eventDate: string | Date;
venue: string;
bannerImageUrl?: string;
min_price: number | string;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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('');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

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.

@HungND-flocus

Copy link
Copy Markdown
Contributor

❌Chưa có loading state khi fetch
❌Icon search bị lỗi
❌Chưa có Responsive

@NguyenVanThanhTung
NguyenVanThanhTung deleted the feat/12-customer-homepage-ui branch April 12, 2026 14:38
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FE] UI Customer: Trang chủ danh sách sự kiện

2 participants