diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate index 6555ef61..15da0371 100644 Binary files a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate and b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift.xcodeproj/project.xcworkspace/xcuserdata/velikho.xcuserdatad/UserInterfaceState.xcuserstate differ diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift new file mode 100644 index 00000000..d3b00513 --- /dev/null +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/ActivityFeed.swift @@ -0,0 +1,193 @@ +// +// ActivityFeed.swift +// AppClientBaseSwift +// +// Recent activity/transaction feed +// Feed hoạt động/giao dịch gần đây +// + +import SwiftUI + +// MARK: - Activity Item +// Item Activity + +/// Activity/Transaction item model +/// Model item hoạt động/giao dịch +struct ActivityItem: Identifiable { + let id = UUID() + let title: String + let subtitle: String + let amount: Double + let icon: String + let color: Color + let time: String + + /// Whether amount is positive (received) + /// Amount có dương (nhận tiền) không + var isPositive: Bool { + amount >= 0 + } + + static let samples: [ActivityItem] = [ + ActivityItem( + title: "Grab - Đặt xe", + subtitle: "Quận 1 → Quận 7", + amount: -45000, + icon: "car.fill", + color: .green, + time: "10:30" + ), + ActivityItem( + title: "Nhận tiền", + subtitle: "Từ Nguyễn Văn A", + amount: 200000, + icon: "arrow.down.circle.fill", + color: .blue, + time: "Hôm qua" + ), + ActivityItem( + title: "Thanh toán điện", + subtitle: "EVN HCM", + amount: -350000, + icon: "bolt.fill", + color: .yellow, + time: "20/01" + ), + ActivityItem( + title: "Hoàn tiền", + subtitle: "Khuyến mãi đặt xe", + amount: 15000, + icon: "gift.fill", + color: .pink, + time: "19/01" + ) + ] +} + +// MARK: - Activity Feed +// Feed Activity + +/// Activity/Transaction feed list +/// List feed hoạt động/giao dịch +struct ActivityFeed: View { + + // MARK: - Properties + + /// Activity items + /// Các items hoạt động + let activities: [ActivityItem] + + /// Item tap callback + /// Callback tap item + var onActivityTap: ((ActivityItem) -> Void)? + + // MARK: - Init + + init(activities: [ActivityItem] = ActivityItem.samples, onActivityTap: ((ActivityItem) -> Void)? = nil) { + self.activities = activities + self.onActivityTap = onActivityTap + } + + // MARK: - Body + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + // Header + HStack { + Text("Hoạt động gần đây") + .font(.headline) + .fontWeight(.semibold) + + Spacer() + + Button("Xem tất cả") { + // Show all activities + } + .font(.subheadline) + .foregroundStyle(.blue) + } + + // Activity list + // Danh sách hoạt động + VStack(spacing: 0) { + ForEach(activities) { activity in + ActivityRow(activity: activity) + .onTapGesture { + onActivityTap?(activity) + } + + if activity.id != activities.last?.id { + Divider() + .padding(.leading, 56) + } + } + } + .background(Color.white) + .cornerRadius(16) + .shadow(color: .black.opacity(0.05), radius: 10, x: 0, y: 5) + } + } +} + +// MARK: - Activity Row +// Row Activity + +/// Individual activity row +/// Row hoạt động riêng lẻ +struct ActivityRow: View { + let activity: ActivityItem + + var body: some View { + HStack(spacing: 14) { + // Icon + ZStack { + Circle() + .fill(activity.color.opacity(0.15)) + .frame(width: 44, height: 44) + + Image(systemName: activity.icon) + .font(.system(size: 18)) + .foregroundStyle(activity.color) + } + + // Content + // Nội dung + VStack(alignment: .leading, spacing: 4) { + Text(activity.title) + .font(.subheadline) + .fontWeight(.medium) + .foregroundStyle(.primary) + + Text(activity.subtitle) + .font(.caption) + .foregroundStyle(.secondary) + } + + Spacer() + + // Amount + Time + VStack(alignment: .trailing, spacing: 4) { + Text(activity.isPositive ? "+\(String.formatVND(activity.amount))" : String.formatVND(activity.amount)) + .font(.subheadline) + .fontWeight(.semibold) + .foregroundStyle(activity.isPositive ? .green : .primary) + + Text(activity.time) + .font(.caption) + .foregroundStyle(.secondary) + } + } + .padding(.horizontal, 16) + .padding(.vertical, 14) + } +} + +// MARK: - Preview + +#Preview { + ZStack { + Color.gray.opacity(0.1).ignoresSafeArea() + ActivityFeed() + .padding() + } +} diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift new file mode 100644 index 00000000..7137b092 --- /dev/null +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/PromoCarousel.swift @@ -0,0 +1,187 @@ +// +// PromoCarousel.swift +// AppClientBaseSwift +// +// Promotional banners carousel +// Carousel banners khuyến mãi +// + +import SwiftUI + +// MARK: - Promo Item +// Item Promo + +/// Promotional banner item +/// Item banner khuyến mãi +struct PromoItem: Identifiable { + let id = UUID() + let title: String + let subtitle: String + let gradientColors: [Color] + let icon: String + + static let samples: [PromoItem] = [ + PromoItem( + title: "Giảm 50% Đặt xe", + subtitle: "Áp dụng đến 31/01", + gradientColors: [.orange, .red], + icon: "car.fill" + ), + PromoItem( + title: "Freeship Đồ ăn", + subtitle: "Đơn từ 50K", + gradientColors: [.green, .mint], + icon: "takeoutbag.and.cup.and.straw.fill" + ), + PromoItem( + title: "Hoàn tiền 20%", + subtitle: "Thanh toán hóa đơn", + gradientColors: [.blue, .purple], + icon: "creditcard.fill" + ) + ] +} + +// MARK: - Promo Carousel +// Carousel Promo + +/// Promotional banners carousel +/// Carousel banners khuyến mãi +struct PromoCarousel: View { + + // MARK: - Properties + + /// Promo items to display + /// Các items promo hiển thị + let items: [PromoItem] + + /// Current page index + /// Index trang hiện tại + @State private var currentIndex = 0 + + /// Auto-scroll timer + /// Timer auto-scroll + @State private var timer: Timer? + + // MARK: - Init + + init(items: [PromoItem] = PromoItem.samples) { + self.items = items + } + + // MARK: - Body + + var body: some View { + VStack(spacing: 12) { + // Carousel + TabView(selection: $currentIndex) { + ForEach(Array(items.enumerated()), id: \.element.id) { index, item in + PromoCard(item: item) + .tag(index) + } + } + .tabViewStyle(.page(indexDisplayMode: .never)) + .frame(height: 140) + + // Page indicators + // Indicators trang + HStack(spacing: 6) { + ForEach(0.. Void)? + + /// Grid columns + /// Các cột grid + private let columns = [ + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()), + GridItem(.flexible()) + ] + + // MARK: - Init + + init(services: [ServiceItem] = ServiceItem.defaultServices, onServiceTap: ((ServiceItem) -> Void)? = nil) { + self.services = services + self.onServiceTap = onServiceTap + } + + // MARK: - Body + + var body: some View { + LazyVGrid(columns: columns, spacing: 20) { + ForEach(services) { service in + ServiceGridItem(service: service) + .onTapGesture { + onServiceTap?(service) + } + } + } + } +} + +// MARK: - Service Grid Item +// Item Grid Service + +/// Individual service grid item +/// Item grid service riêng lẻ +struct ServiceGridItem: View { + let service: ServiceItem + + var body: some View { + VStack(spacing: 10) { + // Icon container + // Container icon + ZStack { + RoundedRectangle(cornerRadius: 16) + .fill(service.color.opacity(0.15)) + .frame(width: 56, height: 56) + + Image(systemName: service.icon) + .font(.system(size: 24)) + .foregroundStyle(service.color) + } + + // Label + Text(service.title) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.primary) + .lineLimit(1) + } + } +} + +// MARK: - Preview + +#Preview { + ServiceGrid() + .padding() +} diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift new file mode 100644 index 00000000..56fae324 --- /dev/null +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Home/WalletCard.swift @@ -0,0 +1,169 @@ +// +// WalletCard.swift +// AppClientBaseSwift +// +// Wallet card with balance and quick actions +// Card ví với số dư và quick actions +// + +import SwiftUI + +// MARK: - Wallet Card +// Card Ví + +/// Wallet card displaying balance and quick actions +/// Card ví hiển thị số dư và quick actions +struct WalletCard: View { + + // MARK: - Properties + + /// Current balance + /// Số dư hiện tại + let balance: Double + + /// Whether balance is hidden + /// Số dư có bị ẩn không + @State private var isBalanceHidden = false + + /// Quick action callback + /// Callback quick action + var onQuickAction: ((QuickAction) -> Void)? + + // MARK: - Quick Actions Enum + + enum QuickAction: String, CaseIterable { + case topUp = "Nạp tiền" + case transfer = "Chuyển tiền" + case payment = "Thanh toán" + + var icon: String { + switch self { + case .topUp: return "plus.circle.fill" + case .transfer: return "arrow.left.arrow.right.circle.fill" + case .payment: return "qrcode" + } + } + } + + // MARK: - Body + + var body: some View { + VStack(spacing: 20) { + // Balance section + // Phần số dư + balanceSection + + // Quick actions + // Quick actions + quickActionsSection + } + .padding(20) + .background( + LinearGradient( + colors: [ + Color(red: 0.4, green: 0.5, blue: 0.92), + Color(red: 0.47, green: 0.3, blue: 0.64) + ], + startPoint: .topLeading, + endPoint: .bottomTrailing + ) + ) + .cornerRadius(20) + .shadow(color: Color(red: 0.4, green: 0.3, blue: 0.7).opacity(0.4), radius: 15, x: 0, y: 10) + } + + // MARK: - Subviews + + /// Balance display section + /// Phần hiển thị số dư + private var balanceSection: some View { + HStack { + VStack(alignment: .leading, spacing: 6) { + Text("Số dư khả dụng") + .font(.subheadline) + .foregroundStyle(.white.opacity(0.8)) + + HStack(spacing: 8) { + if isBalanceHidden { + Text("••••••••") + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } else { + Text(String.formatVND(balance)) + .font(.system(size: 28, weight: .bold, design: .rounded)) + .foregroundStyle(.white) + } + + Button { + withAnimation(.easeInOut(duration: 0.2)) { + isBalanceHidden.toggle() + } + } label: { + Image(systemName: isBalanceHidden ? "eye.slash.fill" : "eye.fill") + .font(.subheadline) + .foregroundStyle(.white.opacity(0.8)) + } + } + } + + Spacer() + + // Points badge + // Badge điểm + VStack(alignment: .trailing, spacing: 4) { + HStack(spacing: 4) { + Image(systemName: "star.circle.fill") + .foregroundStyle(.yellow) + Text("1,250") + .fontWeight(.semibold) + } + .font(.subheadline) + .foregroundStyle(.white) + + Text("điểm thưởng") + .font(.caption) + .foregroundStyle(.white.opacity(0.7)) + } + } + } + + /// Quick actions buttons section + /// Phần buttons quick actions + private var quickActionsSection: some View { + HStack(spacing: 0) { + ForEach(QuickAction.allCases, id: \.self) { action in + Button { + onQuickAction?(action) + } label: { + VStack(spacing: 8) { + ZStack { + Circle() + .fill(.white.opacity(0.2)) + .frame(width: 48, height: 48) + + Image(systemName: action.icon) + .font(.title2) + .foregroundStyle(.white) + } + + Text(action.rawValue) + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.white) + } + } + .frame(maxWidth: .infinity) + } + } + } +} + +// MARK: - Preview + +#Preview { + ZStack { + Color.gray.opacity(0.1).ignoresSafeArea() + WalletCard(balance: 1_250_000) + .padding() + } +} diff --git a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift index 32d8f8f6..63fc689b 100644 --- a/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift +++ b/apps/app-client-base-swift/AppClientBaseSwift/AppClientBaseSwift/Views/Screens/HomeView.swift @@ -2,8 +2,8 @@ // HomeView.swift // AppClientBaseSwift // -// Home screen view with MVVM binding -// View màn hình Home với MVVM binding +// Super App style home screen +// Màn hình home phong cách Super App // import SwiftUI @@ -11,233 +11,211 @@ import SwiftUI // MARK: - Home View // View Home -/// Home tab main view -/// View chính của tab Home +/// Super App style home view with wallet, services, promos +/// View home phong cách Super App với ví, services, promos struct HomeView: View { // MARK: - Properties - /// ViewModel for home screen - /// ViewModel cho màn hình home - @StateObject private var viewModel = HomeViewModel() + /// Auth manager for user info + /// Auth manager cho thông tin user + @StateObject private var authManager = AuthManager.shared + + /// Mock balance + /// Số dư mock + private let balance: Double = 1_250_000 // MARK: - Body var body: some View { NavigationStack { - ScrollView { - VStack(spacing: DesignSystem.spacingLG) { - // Greeting section - // Phần lời chào - greetingSection + ScrollView(showsIndicators: false) { + VStack(spacing: 24) { + // Header + headerSection + .padding(.horizontal, 20) - // Featured carousel - // Carousel nổi bật - if !viewModel.featuredItems.isEmpty { - featuredSection + // Wallet Card + // Card Ví + WalletCard(balance: balance) { action in + handleQuickAction(action) + } + .padding(.horizontal, 20) + + // Services Grid + // Grid Services + VStack(alignment: .leading, spacing: 16) { + Text("Dịch vụ") + .font(.headline) + .fontWeight(.semibold) + .padding(.horizontal, 20) + + ServiceGrid { service in + handleServiceTap(service) + } + .padding(.horizontal, 20) } - // Main content - // Nội dung chính - contentSection + // Promo Carousel + // Carousel Promo + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("Ưu đãi hấp dẫn") + .font(.headline) + .fontWeight(.semibold) + + Spacer() + + Button("Xem tất cả") { + // Show all promos + } + .font(.subheadline) + .foregroundStyle(.blue) + } + .padding(.horizontal, 20) + + PromoCarousel() + .padding(.horizontal, 16) + } + + // Activity Feed + // Feed Hoạt động + ActivityFeed { activity in + handleActivityTap(activity) + } + .padding(.horizontal, 20) + + // Bottom spacing + Spacer(minLength: 20) } - .padding(.horizontal, DesignSystem.spacingMD) - .padding(.vertical, DesignSystem.spacingSM) + .padding(.top, 10) } - .refreshable { - await viewModel.refresh() - } - .navigationTitle("tab_home".localized) - .navigationBarTitleDisplayMode(.large) - .loadingOverlay(viewModel.isLoading) - .task { - await viewModel.loadData() - } - .alert("error_title".localized, isPresented: .constant(viewModel.errorMessage != nil)) { - Button("common_ok".localized) { - viewModel.errorMessage = nil + .background(Color(red: 0.96, green: 0.97, blue: 0.98)) + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + HStack(spacing: 16) { + // QR Code + Button { + // Show QR scanner + } label: { + Image(systemName: "qrcode.viewfinder") + .font(.title3) + .foregroundStyle(.primary) + } + + // Notifications + Button { + // Show notifications + } label: { + Image(systemName: "bell.badge") + .font(.title3) + .foregroundStyle(.primary) + } + } } - } message: { - Text(viewModel.errorMessage ?? "") } } } // MARK: - Subviews - /// Greeting section at top - /// Phần lời chào ở đầu - private var greetingSection: some View { - HStack { - VStack(alignment: .leading, spacing: DesignSystem.spacingXS) { - Text(viewModel.greeting) - .font(.title2) - .fontWeight(.bold) + /// Header with user info + /// Header với thông tin user + private var headerSection: some View { + HStack(spacing: 14) { + // Avatar + avatarView - Text("home_subtitle".localized) + // Greeting + VStack(alignment: .leading, spacing: 4) { + Text(greetingText) .font(.subheadline) .foregroundStyle(.secondary) + + Text(authManager.currentUser?.name ?? "Người dùng") + .font(.title3) + .fontWeight(.bold) } Spacer() - - // Notification button - // Nút thông báo - Button { - // Handle notification tap - // Xử lý tap thông báo - } label: { - Image(systemName: "bell.badge") - .font(.title2) - .foregroundStyle(.primary) - } } - .padding(.vertical, DesignSystem.spacingSM) } - /// Featured items carousel section - /// Phần carousel items nổi bật - private var featuredSection: some View { - VStack(alignment: .leading, spacing: DesignSystem.spacingSM) { - Text("home_featured".localized) - .font(.headline) - - ScrollView(.horizontal, showsIndicators: false) { - HStack(spacing: DesignSystem.spacingMD) { - ForEach(viewModel.featuredItems) { item in - FeaturedCard(item: item) - .onTapGesture { - viewModel.handleItemTap(item) - } - } + /// User avatar view + /// View avatar user + private var avatarView: some View { + Group { + if let avatarUrl = authManager.currentUser?.avatarUrl, + let url = URL(string: avatarUrl) { + AsyncImage(url: url) { image in + image + .resizable() + .aspectRatio(contentMode: .fill) + } placeholder: { + avatarPlaceholder } + } else { + avatarPlaceholder } } + .frame(width: 50, height: 50) + .clipShape(Circle()) } - /// Main content section with items - /// Phần nội dung chính với các items - private var contentSection: some View { - VStack(alignment: .leading, spacing: DesignSystem.spacingSM) { - Text("home_explore".localized) - .font(.headline) - - LazyVStack(spacing: DesignSystem.spacingMD) { - ForEach(viewModel.items) { item in - HomeItemRow(item: item) - .onTapGesture { - viewModel.handleItemTap(item) - } - } - } - } - } -} - -// MARK: - Featured Card -// Card nổi bật - -/// Featured item card view -/// View card item nổi bật -struct FeaturedCard: View { - let item: HomeItem - - var body: some View { - VStack(alignment: .leading, spacing: DesignSystem.spacingSM) { - // Placeholder image - // Ảnh placeholder - RoundedRectangle(cornerRadius: DesignSystem.cornerRadiusMD) + /// Avatar placeholder + /// Placeholder avatar + private var avatarPlaceholder: some View { + ZStack { + Circle() .fill( LinearGradient( - colors: [.blue.opacity(0.8), .purple.opacity(0.8)], + colors: [.blue, .purple], startPoint: .topLeading, endPoint: .bottomTrailing ) ) - .frame(width: 280, height: 140) - .overlay( - Image(systemName: "star.fill") - .font(.largeTitle) - .foregroundStyle(.white) - ) - VStack(alignment: .leading, spacing: DesignSystem.spacingXS) { - Text(item.title) - .font(.subheadline) - .fontWeight(.semibold) - .lineLimit(1) - - Text(item.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - .lineLimit(2) - } - .frame(width: 280, alignment: .leading) + Text(authManager.currentUser?.initials ?? "U") + .font(.headline) + .fontWeight(.bold) + .foregroundStyle(.white) } } -} -// MARK: - Home Item Row -// Row item Home - -/// Home item row view -/// View row item home -struct HomeItemRow: View { - let item: HomeItem - - var body: some View { - HStack(spacing: DesignSystem.spacingMD) { - // Icon - RoundedRectangle(cornerRadius: DesignSystem.cornerRadiusSM) - .fill(Color.accentColor.opacity(0.1)) - .frame(width: 56, height: 56) - .overlay( - Image(systemName: iconForCategory(item.category)) - .font(.title2) - .foregroundStyle(Color.accentColor) - ) - - // Content - // Nội dung - VStack(alignment: .leading, spacing: DesignSystem.spacingXS) { - Text(item.title) - .font(.subheadline) - .fontWeight(.medium) - - Text(item.subtitle) - .font(.caption) - .foregroundStyle(.secondary) - } - - Spacer() - - // Arrow - Image(systemName: "chevron.right") - .font(.caption) - .foregroundStyle(.tertiary) - } - .padding(DesignSystem.spacingMD) - .cardStyle() - } - - /// Get SF Symbol icon for category - /// Lấy icon SF Symbol cho category - /// - Parameter category: Item category / Category của item - /// - Returns: SF Symbol name / Tên SF Symbol - private func iconForCategory(_ category: String) -> String { - switch category { - case "explore": - return "map" - case "promo": - return "tag.fill" - case "points": - return "star.circle.fill" - case "recommend": - return "heart.fill" + /// Greeting text based on time of day + /// Text lời chào dựa trên thời gian trong ngày + private var greetingText: String { + let hour = Calendar.current.component(.hour, from: Date()) + switch hour { + case 5..<12: + return "Chào buổi sáng 👋" + case 12..<18: + return "Chào buổi chiều ☀️" default: - return "circle.fill" + return "Chào buổi tối 🌙" } } + + // MARK: - Methods + + /// Handle quick action tap + /// Xử lý tap quick action + private func handleQuickAction(_ action: WalletCard.QuickAction) { + print("Quick action: \(action.rawValue)") + } + + /// Handle service tap + /// Xử lý tap service + private func handleServiceTap(_ service: ServiceItem) { + print("Service: \(service.title)") + } + + /// Handle activity tap + /// Xử lý tap activity + private func handleActivityTap(_ activity: ActivityItem) { + print("Activity: \(activity.title)") + } } // MARK: - Preview diff --git a/note.md b/note.md index 2446f0ee..00b2854b 100644 --- a/note.md +++ b/note.md @@ -2,7 +2,7 @@ Test Account Tài khoản: hongochai10@icloud.com Mật Khẩu: Velik@2026 -demo@goodgo.vn / Demo@123. +admin@goodgo.com / 123456 dotnet build -c Debug -f net10.0-ios -t:Run -p:_DeviceName=:v2:udid=D8A27496-0AFB-4314-96EC-E8B685575330 curl -s -X POST "http