setSelectedCategory(selectedCategory === cat.k ? null : cat.k)}
+ className={`glass-card hover:bg-white transition-all cursor-pointer p-6 rounded-2xl flex items-center gap-4 group border-2 ${
+ selectedCategory === cat.k ? "border-primary bg-white shadow-lg -translate-y-1" : "border-transparent"
+ }`}
>
- Hot Events
+ {searchQuery ? `Kết quả cho "${searchQuery}"` : "Sự kiện xu hướng"}
@@ -124,17 +222,25 @@ function Home() {
Loading Premium Events...
- ) : (
+ ) : filteredEvents && filteredEvents.length > 0 ? (
- {events?.map((event) => (
+ {filteredEvents.map((event) => (
))}
+ ) : (
+
+
+
+
+
Không tìm thấy sự kiện nào phù hợp.
+
Xem tất cả sự kiện
+
)}
- View All Events
+ Xem thêm
diff --git a/frontend/src/pages/MyTickets.jsx b/frontend/src/pages/MyTickets.jsx
new file mode 100644
index 0000000..9cfbd15
--- /dev/null
+++ b/frontend/src/pages/MyTickets.jsx
@@ -0,0 +1,229 @@
+import { useState } from "react";
+import {
+ ArrowLeft,
+ Calendar,
+ MapPin,
+ MoreVertical,
+ Ticket,
+ ArrowRight,
+ Timer,
+ Check,
+ Filter,
+ ChevronRight
+} from "lucide-react";
+import { Link } from "react-router-dom";
+
+export default function MyTickets() {
+ const [activeTab, setActiveTab] = useState("all");
+ const [activeFilter, setActiveFilter] = useState("upcoming");
+
+ const tickets = [
+ {
+ id: "TR-20241012",
+ title: "Neon Horizon Tour",
+ status: "paid",
+ statusText: "ĐÃ THANH TOÁN",
+ time: "20:00, 24 Tháng 10, 2024",
+ location: "Trung tâm Triển lãm SECC",
+ image: "https://lh3.googleusercontent.com/aida-public/AB6AXuAx5WJbxA9nN_OQ_PS7mF-zNvR23DXUB9DQM6oemTCd4N84g_PlNw5JJJpnpRaKlipe9YcuFgDVk1a1ljk3C7sqvnFw0XJjT0G42IH7Mjy54-CfvGtOoCOiQdyq4ufAkz2epbxwKGiUbe6ZymW-cUUAWYgUCa35WOdn7WySUZF3zEFHh0VKJWpNbrnWvYEfBHYOMEvAvimtNzPbU8dX536IzZVR7YAJ-Zp5xbXx2NbR6xYS2sJygK6HStTsXHTuOnmH3ksclWPm5hE",
+ seats: ["Seat F12", "Seat F13"]
+ },
+ {
+ id: "TR-20241015",
+ title: "Neon Horizon Tour",
+ status: "pending",
+ statusText: "CHỜ THANH TOÁN",
+ time: "20:00, 24 Tháng 10, 2024",
+ location: "Trung tâm Triển lãm SECC",
+ image: "https://lh3.googleusercontent.com/aida-public/AB6AXuALGoWo3Qx5fvGQ6LjZWGOXJjUPkRqaDPx0iMAH7NNvZXQcnWOVXwGqtvgr_a-K0rRZ6Pd78hCeAIJCigfP2P1wFWlPO7c3us1FLCUBlNG2yDmksV9OTZCZ3EPXlSnY2Away4vuUSv-9nTABwJ-E91nxIzCRA4ek2RN6j7SXYE3woN0lIEvSvUOeF4Vnwa11AG5HvA2jpunQ1QstZEYjkiXz8__aUnjaF-4k8_M8l975IuHZ_NQdqj91n05xwPNVYzhKfRxGvw5G34",
+ ticketType: "Standard Access (x2)",
+ total: "2.400.000đ",
+ timeLeft: "09:54"
+ },
+ {
+ id: "TR-20240988",
+ title: "Symphony of the Void",
+ status: "paid",
+ statusText: "ĐÃ THANH TOÁN",
+ time: "19:30, 02 Tháng 11, 2024",
+ location: "Nhà hát lớn Thành phố",
+ image: "https://lh3.googleusercontent.com/aida-public/AB6AXuB1W2EYD1WLw_I3SXWlPjedNWAnUycBgXSNE2ieq-LNvDK6zKMVf2nacuBqJVarIvO1NqAZDW6Ua1ySPTxRVJHHW_wTwODYRltwCX_ksifg-QPM-RzLOwEsPzioDKA_RnwAP8tFuKuQRHp6RPC9eKshndOdpxl6k7SCmd9QPtGwH-5aplSbZqNmALEjGLFYvig0WfEbGRwRUeaJZQRjeN1vPxea06-MqLTc_TxcpxCra3ULaY36c_FsDtwjwoyBBOAUCE5JsBKgt2I",
+ readyMessage: "Vé đã sẵn sàng trong hòm thư của bạn"
+ }
+ ];
+
+ return (
+
+
+
+
+
+ {/* Filter Tabs */}
+
+
setActiveTab("all")}
+ className={`pb-4 text-sm font-bold transition-all relative whitespace-nowrap ${activeTab === 'all' ? 'text-primary' : 'text-muted hover:text-text'}`}
+ >
+ Tất cả
+ {activeTab === 'all' &&
}
+
+
setActiveTab("unpaid")}
+ className={`pb-4 text-sm font-bold transition-all relative whitespace-nowrap ${activeTab === 'unpaid' ? 'text-primary' : 'text-muted hover:text-text'}`}
+ >
+ Chưa hoàn tất thanh toán
+ {activeTab === 'unpaid' &&
}
+
+
setActiveTab("success")}
+ className={`pb-4 text-sm font-bold transition-all relative whitespace-nowrap ${activeTab === 'success' ? 'text-primary' : 'text-muted hover:text-text'}`}
+ >
+ Thành công
+ {activeTab === 'success' &&
}
+
+
+
+ {/* Filter Chips & Actions */}
+
+
+ setActiveFilter("upcoming")}
+ className={`px-6 py-2 rounded-xl text-sm font-bold transition-all ${activeFilter === 'upcoming' ? 'bg-primary text-white shadow-lg shadow-primary/20' : 'bg-white border border-gray-100 text-muted hover:border-primary/30 hover:text-primary'}`}
+ >
+ Sắp diễn ra
+
+ setActiveFilter("past")}
+ className={`px-6 py-2 rounded-xl text-sm font-bold transition-all ${activeFilter === 'past' ? 'bg-primary text-white shadow-lg shadow-primary/20' : 'bg-white border border-gray-100 text-muted hover:border-primary/30 hover:text-primary'}`}
+ >
+ Đã kết thúc
+
+
+
+
+
+ Lọc theo: Mới nhất
+
+
+
+ {/* Ticket List */}
+
+ {tickets.map((ticket, index) => (
+
+
+
+
+
+
+
+
+
+
+
{ticket.title}
+ {ticket.status === 'paid' ? (
+
+ {ticket.statusText}
+
+ ) : (
+
+
+ Tiếp tục thanh toán
+
+
+
+ {ticket.timeLeft}
+
+
+ )}
+
+
+
Mã đơn hàng: {ticket.id}
+
+
+
+
+ {ticket.time}
+
+
+
+ {ticket.location}
+
+
+
+ {ticket.readyMessage && (
+
+
+
+
+
{ticket.readyMessage}
+
+ )}
+
+
+
+
+
+
+
+ {ticket.status === 'paid' && (
+
+ Chi tiết
+
+ )}
+
+
+
+ {/* Bottom Section for Seats or Payment Info */}
+ {ticket.seats && (
+
+ {ticket.seats.map((seat, idx) => (
+
+ ))}
+
+ )}
+
+ {ticket.status === 'pending' && (
+
+
+
+ Loại vé:
+ {ticket.ticketType}
+
+
+ Tạm tính:
+ {ticket.total}
+
+
+
Hủy đơn hàng
+
+ )}
+
+
+ ))}
+
+
+ {/* Empty State Mock / Footer Space */}
+
+
+ Tải thêm
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx
new file mode 100644
index 0000000..bb2117f
--- /dev/null
+++ b/frontend/src/pages/Profile.jsx
@@ -0,0 +1,245 @@
+import { useState, useEffect, useRef } from "react";
+import { Camera, Check, ChevronDown, Loader2 } from "lucide-react";
+import { useAuth } from "../hooks/useAuth";
+import { profileApi } from "../api/profileApi";
+
+export default function Profile() {
+ const { user, refreshProfile } = useAuth();
+ const fileInputRef = useRef(null);
+
+ const [profile, setProfile] = useState({
+ fullName: "",
+ phoneNumber: "",
+ email: "",
+ dateOfBirth: "",
+ gender: "Male",
+ avatarUrl: ""
+ });
+
+ const [loading, setLoading] = useState(true);
+ const [saving, setSaving] = useState(false);
+ const [uploading, setUploading] = useState(false);
+ const [message, setMessage] = useState({ type: "", text: "" });
+
+ useEffect(() => {
+ fetchProfile();
+ }, []);
+
+ const fetchProfile = async () => {
+ try {
+ setLoading(true);
+ const data = await profileApi.getProfile();
+ setProfile({
+ fullName: data.fullName || "",
+ phoneNumber: data.phoneNumber || "",
+ email: data.email || user?.email || "",
+ dateOfBirth: data.dateOfBirth || "",
+ gender: data.gender || "Male",
+ avatarUrl: data.avatarUrl || ""
+ });
+ } catch (error) {
+ console.error("Failed to load profile", error);
+ setMessage({ type: "error", text: "Không thể tải thông tin hồ sơ." });
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ const handleChange = (e) => {
+ const { name, value } = e.target;
+ setProfile(prev => ({ ...prev, [name]: value }));
+ };
+
+ const handleAvatarChange = async (e) => {
+ const file = e.target.files[0];
+ if (!file) return;
+
+ try {
+ setUploading(true);
+ const publicUrl = await profileApi.uploadAvatar(file, user?.id);
+ setProfile(prev => ({ ...prev, avatarUrl: publicUrl }));
+ refreshProfile(); // Sync with global context (Navbar)
+ setMessage({ type: "success", text: "Cập nhật ảnh đại diện thành công!" });
+ } catch (error) {
+ console.error("Upload error", error);
+ setMessage({ type: "error", text: "Lỗi tải ảnh lên." });
+ } finally {
+ setUploading(false);
+ }
+ };
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ try {
+ setSaving(true);
+ setMessage({ type: "", text: "" });
+ await profileApi.updateProfile(profile);
+ refreshProfile(); // Sync with global context (Navbar)
+ setMessage({ type: "success", text: "Cập nhật thông tin thành công!" });
+ } catch (error) {
+ console.error("Update error", error);
+ setMessage({ type: "error", text: "Lỗi khi lưu thông tin." });
+ } finally {
+ setSaving(false);
+ }
+ };
+
+ if (loading) {
+ return (
+
+
+
+ );
+ }
+
+ return (
+
+
+ {/* Decorative Profile Header */}
+
+
+
+ {/* Avatar Section Inside Header */}
+
+
+
+ {profile.avatarUrl ? (
+
+ ) : (
+
+ {profile.fullName?.charAt(0) || user?.email?.charAt(0) || "U"}
+
+ )}
+
+
+
fileInputRef.current?.click()}
+ disabled={uploading}
+ className="absolute bottom-0 right-0 bg-white text-primary p-1.5 rounded-full shadow-lg border border-gray-100 hover:bg-gray-100 transition-all active:scale-90 disabled:opacity-50 z-20"
+ title="Đổi ảnh đại diện"
+ >
+ {uploading ? : }
+
+
+
+
+
+
+
+
+
+ {/* Message Alert */}
+ {message.text && (
+
+ {message.text}
+
+ )}
+
+ {/* Form */}
+
+
+
+
+ );
+}