diff --git a/apps/web/.storybook/main.ts b/apps/web/.storybook/main.ts new file mode 100644 index 00000000..7e34cbdc --- /dev/null +++ b/apps/web/.storybook/main.ts @@ -0,0 +1,19 @@ +import type { StorybookConfig } from '@storybook/nextjs'; + +const config: StorybookConfig = { + stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'], + addons: [ + '@storybook/addon-links', + '@storybook/addon-essentials', + '@storybook/addon-interactions', + ], + framework: { + name: '@storybook/nextjs', + options: {}, + }, + docs: { + autodocs: 'tag', + }, + staticDirs: ['../public'], +}; +export default config; diff --git a/apps/web/.storybook/preview.ts b/apps/web/.storybook/preview.ts new file mode 100644 index 00000000..8c0be08d --- /dev/null +++ b/apps/web/.storybook/preview.ts @@ -0,0 +1,17 @@ +import type { Preview } from '@storybook/react'; + +import '../src/app/globals.css'; + +const preview: Preview = { + parameters: { + actions: { argTypesRegex: '^on[A-Z].*' }, + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/i, + }, + }, + }, +}; + +export default preview; diff --git a/apps/web/biome_errors.txt b/apps/web/biome_errors.txt new file mode 100644 index 00000000..62e85af1 --- /dev/null +++ b/apps/web/biome_errors.txt @@ -0,0 +1,110 @@ +src/app/tenant-dashboard/page.tsx:119:39 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected any. Specify a different type. + + 117 │ }, []); + 118 │ + > 119 │ const handleViewDetails = (booking: any): void => { + │ ^^^ + 120 │ // Legacy mapping or handle directly + 121 │ const found = apiBookings.find((b) => b.id === booking.id); + + i any disables many type checking rules. Its use should be avoided. + + +src/app/tenant-dashboard/page.tsx:128:41 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected any. Specify a different type. + + 126 │ }; + 127 │ + > 128 │ const handleCancelBooking = (booking: any): void => { + │ ^^^ + 129 │ const found = apiBookings.find((b) => b.id === booking.id); + 130 │ if (found) { + + i any disables many type checking rules. Its use should be avoided. + + +src/app/tenant-dashboard/page.tsx:359:29 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected any. Specify a different type. + + 357 │ ) : user ? ( + 358 │ 359 │ user={user as any} + │ ^^^ + 360 │ onUpdateProfile={handleUpdateUser as any} + 361 │ onUploadAvatar={async (file) => { + + i any disables many type checking rules. Its use should be avoided. + + +src/app/tenant-dashboard/page.tsx:360:52 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected any. Specify a different type. + + 358 │ 360 │ onUpdateProfile={handleUpdateUser as any} + │ ^^^ + 361 │ onUploadAvatar={async (file) => { + 362 │ await apiUploadAvatar(user.id.toString(), file); + + i any disables many type checking rules. Its use should be avoided. + + +src/app/tenant-dashboard/page.tsx:396:45 lint/suspicious/noExplicitAny ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Unexpected any. Specify a different type. + + 394 │ walletBalance={walletBalance} + 395 │ pendingTransactions={pendingTransactions} + > 396 │ transactions={transactions as any} + │ ^^^ + 397 │ onExportTransactions={apiExportTransactions} + 398 │ /> + + i any disables many type checking rules. Its use should be avoided. + + +src/app/tenant-dashboard/page.tsx format ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Formatter would have printed the following content: + + 207 207 │
+ 208 208 │
+ 214 215 │
+ ······· │ + 316 317 │ type="button" + 317 318 │ onClick={() => setActiveTab(tab.id)} + 318 │ - ··················className={`flex·items-center·space-x-2·py-4·px-1·border-b-2·font-medium·text-sm·${activeTab·===·tab.id + 319 │ - ····················?·'border-blue-500·text-blue-600·dark:text-blue-400' + 320 │ - ····················:·'border-transparent·text-gray-500·dark:text-white·hover:border-gray-300' + 321 │ - ····················}`} + 319 │ + ··················className={`flex·items-center·space-x-2·py-4·px-1·border-b-2·font-medium·text-sm·${ + 320 │ + ····················activeTab·===·tab.id + 321 │ + ······················?·'border-blue-500·text-blue-600·dark:text-blue-400' + 322 │ + ······················:·'border-transparent·text-gray-500·dark:text-white·hover:border-gray-300' + 323 │ + ··················}`} + 322 324 │ > + 323 325 │ + + +Checked 1 file in 7ms. No fixes applied. +Found 6 errors. +check ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ + + × Some errors were emitted while running checks. + + diff --git a/apps/web/package.json b/apps/web/package.json index 8ad2fd90..a911eae1 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -9,7 +9,11 @@ "lint": "next lint", "test:e2e": "playwright test", "test:e2e:ui": "playwright test --ui", - "test:e2e:headed": "playwright test --headed" + "test:e2e:headed": "playwright test --headed", + "storybook": "storybook dev -p 6006", + "build-storybook": "storybook build", + "test": "vitest run", + "test:watch": "vitest" }, "dependencies": { "stellar-social-sdk": "file:./stellar-social-sdk", @@ -51,16 +55,32 @@ }, "devDependencies": { "@playwright/test": "^1.55.0", + "@storybook/addon-essentials": "8.6.14", + "@storybook/addon-interactions": "8.6.14", + "@storybook/addon-links": "8.6.14", + "@storybook/blocks": "8.6.14", + "@storybook/nextjs": "8.6.14", + "@storybook/react": "8.6.14", + "@storybook/test": "8.6.14", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "14.2.2", + "@testing-library/user-event": "14.5.2", + "@types/jest": "^30.0.0", + "@types/jsdom": "^27.0.0", "@types/leaflet": "^1.9.19", "@types/node": "^20", "@types/react": "^18", "@types/react-dom": "^18", + "@vitejs/plugin-react": "^5.1.2", "autoprefixer": "^10.4.17", "eslint": "^8", "eslint-config-next": "14.1.0", + "jsdom": "^27.4.0", "node-loader": "^2.1.0", "postcss": "^8.4.35", + "storybook": "8.6.14", "tailwindcss": "^3.4.1", - "typescript": "^5" + "typescript": "^5", + "vitest": "^4.0.18" } } diff --git a/apps/web/src/app/booking/page.tsx b/apps/web/src/app/booking/page.tsx index 17f03d44..702effa7 100644 --- a/apps/web/src/app/booking/page.tsx +++ b/apps/web/src/app/booking/page.tsx @@ -3,7 +3,6 @@ import { BookingConfirmation } from '@/components/booking/BookingConfirmation'; import { BookingForm } from '@/components/booking/BookingForm'; import { WalletConnectionModal } from '@/components/booking/WalletConnectionModal'; import { useWallet } from '@/hooks/useWallet'; -import { useTheme } from 'next-themes'; import { useRouter } from 'next/navigation'; import { useState } from 'react'; import type { DateRange } from 'react-day-picker'; @@ -20,10 +19,8 @@ interface BookingPageProps { type BookingFlowStep = 'form' | 'payment' | 'confirmation'; export default function BookingPage({ params }: BookingPageProps) { - const { theme: _theme } = useTheme(); const _router = useRouter(); const { isConnected, connect, publicKey } = useWallet(); - const [_selectedDates, _setSelectedDates] = useState({ from: undefined, to: undefined, @@ -54,7 +51,7 @@ export default function BookingPage({ params }: BookingPageProps) { const _property = { id: params.propertyId, title: 'Luxury Beachfront Villa', - image: '/images/property-placeholder.jpg', + image: '/images/property-placeholder.webp', pricePerNight: 150, deposit: 500, commission: 0.00001, @@ -107,8 +104,10 @@ export default function BookingPage({ params }: BookingPageProps) { propertyId: data.property.id, userId: publicKey, dates: data.dates, + checkIn: data.dates.from.toISOString(), + checkOut: data.dates.to.toISOString(), guests: data.guests, - total: data.totalAmount, + totalAmount: data.totalAmount, deposit: data.depositAmount, }); @@ -116,12 +115,12 @@ export default function BookingPage({ params }: BookingPageProps) { toast.success('Booking created! Proceeding to payment.'); setCurrentBookingData({ - bookingId: createdBooking.bookingId, + bookingId: createdBooking.data.id, property: data.property, dates: data.dates, guests: data.guests, totalAmount: data.totalAmount, - escrowAddress: createdBooking.escrowAddress, + escrowAddress: createdBooking.data.escrowAddress || '', }); setBookingStep('payment'); diff --git a/apps/web/src/app/dashboard/host-dashboard/page.tsx b/apps/web/src/app/dashboard/host-dashboard/page.tsx index b182583f..02f87a54 100644 --- a/apps/web/src/app/dashboard/host-dashboard/page.tsx +++ b/apps/web/src/app/dashboard/host-dashboard/page.tsx @@ -6,7 +6,7 @@ import ProfileManagement from '@/components/dashboard/ProfileManagement'; import PropertyManagement from '@/components/dashboard/PropertyManagement'; import { RoleGuard } from '@/components/guards/RoleGuard'; import { useRealTimeNotifications } from '@/hooks/useRealTimeUpdates'; -import { Calendar, DollarSign, Settings, User, Wallet } from 'lucide-react'; +import { Calendar, DollarSign, Loader2, RefreshCw, Settings, User, Wallet } from 'lucide-react'; import Image from 'next/image'; import { useState } from 'react'; import { AddPropertyModal } from './components/AddPropertyModal'; @@ -17,15 +17,20 @@ import { EarningsStats } from './components/EarningsStats'; import { PaymentMethods } from './components/PaymentMethods'; import { PayoutHistory } from './components/PayoutHistory'; import { RecentTransactions } from './components/RecentTransactions'; -import { mockBookings, mockEarnings, mockProperties, mockUser } from './mockData'; +import { mockEarnings, mockProperties, mockUser } from './mockData'; import type { Property, UserProfile } from './types'; +import { ErrorDisplay } from '@/components/ui/error-display'; +import { useDashboard } from '@/hooks/useDashboard'; +import { type UserProfile as ApiUserProfile } from '@/types'; +import type { Booking } from '@/types/shared'; +import Breadcrumb from '~/components/ui/breadcrumb'; + const HostDashboard = () => { const [activeTab, setActiveTab] = useState('properties'); - const [properties, setProperties] = useState(mockProperties); - const [selectedProperty, _setSelectedProperty] = useState(null); const [showCalendarModal, setShowCalendarModal] = useState(false); const [showAddPropertyModal, setShowAddPropertyModal] = useState(false); + const [selectedProperty, _setSelectedProperty] = useState(null); const [selectedDates, setSelectedDates] = useState>(new Set()); const [newProperty, setNewProperty] = useState({ title: '', @@ -40,8 +45,38 @@ const HostDashboard = () => { images: [] as string[], rules: '', }); - const [user, setUser] = useState(mockUser); - const [bookings, setBookings] = useState(mockBookings); + + const { + bookings: apiBookings, + user: apiUser, + isLoadingBookings, + isLoadingProfile, + bookingsError, + profileError, + refetchAll, + cancelBooking: apiCancelBooking, + updateProfile: apiUpdateProfile, + uploadAvatar: apiUploadAvatar, + } = useDashboard({ userId: 'host-1', userType: 'host' }); + + const user = apiUser || mockUser; + const bookings: Booking[] = apiBookings.map((b) => ({ + id: b.id, + propertyTitle: b.propertyTitle, + propertyImage: b.propertyImage, + location: b.propertyLocation, + checkIn: b.checkIn, + checkOut: b.checkOut, + guests: b.guests, + totalAmount: b.totalAmount, + status: b.status as 'pending' | 'confirmed' | 'completed' | 'cancelled', + bookingDate: b.bookingDate, + propertyId: '1', + canCancel: true, + canReview: b.status === 'completed', + })); + + const [properties, setProperties] = useState(mockProperties); const { notifications, @@ -51,7 +86,11 @@ const HostDashboard = () => { markAllAsRead: handleMarkAllAsRead, deleteNotification: handleDeleteNotification, deleteAllNotifications: handleDeleteAllNotifications, - } = useRealTimeNotifications(user.id); + } = useRealTimeNotifications(user.id.toString()); + + const handleRefresh = async () => { + await refetchAll(); + }; const handleAddProperty = (e: React.FormEvent) => { e.preventDefault(); @@ -116,25 +155,16 @@ const HostDashboard = () => { const handleCancelBooking = async (bookingId: string) => { try { - await new Promise((resolve) => setTimeout(resolve, 1000)); - - setBookings((prev) => - prev.map((booking) => - booking.id === bookingId - ? { ...booking, status: 'cancelled' as const, canCancel: false } - : booking - ) - ); - + await apiCancelBooking(bookingId); addNotification({ id: Date.now().toString(), - type: 'booking', + type: 'booking' as const, title: 'Booking Cancelled', message: 'A booking has been cancelled', priority: 'medium', isRead: false, createdAt: new Date().toISOString(), - userId: user.id, + userId: user.id.toString(), }); } catch (error) { console.error('Failed to cancel booking:', error); @@ -143,19 +173,32 @@ const HostDashboard = () => { const handleUpdateProfile = async (updatedProfile: Partial) => { try { - await new Promise((resolve) => setTimeout(resolve, 1000)); + if (!apiUser) return; + + const safeUpdates = { + ...updatedProfile, + preferences: updatedProfile.preferences + ? { + currency: apiUser.preferences?.currency || 'USD', + language: apiUser.preferences?.language || 'en', + notifications: updatedProfile.preferences.notifications, + emailNotifications: updatedProfile.preferences.emailUpdates, + marketingEmails: apiUser.preferences?.marketingEmails, + } + : apiUser.preferences, + }; - setUser((prev) => ({ ...prev, ...updatedProfile })); + await apiUpdateProfile({ ...apiUser, ...safeUpdates } as unknown as ApiUserProfile); addNotification({ id: Date.now().toString(), - type: 'system', + type: 'system' as const, title: 'Profile Updated', message: 'Your profile has been successfully updated', priority: 'low', isRead: false, createdAt: new Date().toISOString(), - userId: user.id, + userId: user.id.toString(), }); } catch (error) { console.error('Failed to update profile:', error); @@ -164,20 +207,16 @@ const HostDashboard = () => { const handleUploadAvatar = async (file: File) => { try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const avatarUrl = URL.createObjectURL(file); - setUser((prev) => ({ ...prev, avatar: avatarUrl })); - + await apiUploadAvatar(user.id.toString(), file); addNotification({ id: Date.now().toString(), - type: 'system', + type: 'system' as const, title: 'Avatar Updated', message: 'Your profile picture has been successfully updated', priority: 'low', isRead: false, createdAt: new Date().toISOString(), - userId: user.id, + userId: user.id.toString(), }); } catch (error) { console.error('Failed to upload avatar:', error); @@ -205,8 +244,20 @@ const HostDashboard = () => {

Host Dashboard

+ {(isLoadingBookings || isLoadingProfile) && ( + + )}
+ ({ @@ -316,14 +367,23 @@ const HostDashboard = () => {

- {/* Statistics Cards */} - - - + {bookingsError ? ( + + ) : ( + <> + + + + )}
)} @@ -348,14 +408,35 @@ const HostDashboard = () => {
)} - {activeTab === 'profile' && ( - - )} + {activeTab === 'profile' && + (profileError ? ( + + ) : ( + + ))} {activeTab === 'wallet' && (
diff --git a/apps/web/src/app/dashboard/tenant-dashboard/page.tsx b/apps/web/src/app/dashboard/tenant-dashboard/page.tsx index 7717a5ff..d0e34220 100644 --- a/apps/web/src/app/dashboard/tenant-dashboard/page.tsx +++ b/apps/web/src/app/dashboard/tenant-dashboard/page.tsx @@ -3,7 +3,14 @@ import BookingHistory from '@/components/dashboard/BookingHistory'; import NotificationSystem from '@/components/dashboard/NotificationSystem'; import ProfileManagement from '@/components/dashboard/ProfileManagement'; +import { ErrorDisplay } from '@/components/ui/error-display'; +import { LoadingGrid } from '@/components/ui/loading-skeleton'; import { RoleGuard } from '@/components/guards/RoleGuard'; +import { useDashboard } from '@/hooks/useDashboard'; +import type { UserProfile as ApiUserProfile } from '@/types'; +import type { UserProfile, Booking } from '@/types/shared'; +// import { Breadcrumb } from '@/components/ui/breadcrumb'; + import { BarChart3, Calendar, @@ -16,10 +23,14 @@ import { Filter, Home, Info, + Loader2, LogOut, MapPin, MessageSquare, PieChart, + Plus, + RefreshCw, + Search, Settings, Star, User, @@ -30,113 +41,6 @@ import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { useAuth } from '~/hooks/auth/use-auth'; -interface Booking { - id: string; - propertyTitle: string; - propertyImage: string; - location: string; - checkIn: string; - checkOut: string; - guests: number; - totalAmount: number; - status: 'pending' | 'confirmed' | 'completed' | 'cancelled'; - bookingDate: string; - propertyId: string; - escrowAddress?: string; - transactionHash?: string; - canCancel: boolean; - canReview: boolean; -} - -interface UserProfile { - id: string; - name: string; - email: string; - avatar: string; - phone?: string; - location?: string; - bio?: string; - memberSince: string; - totalBookings: number; - totalSpent: number; - preferences: { - notifications: boolean; - emailUpdates: boolean; - pushNotifications: boolean; - }; -} - -// Mock data for demonstration -const mockBookings: Booking[] = [ - { - id: '1', - propertyTitle: 'Luxury Downtown Apartment', - propertyImage: - 'https://images.unsplash.com/photo-1522708323590-d24dbb6b0267?w=800&auto=format&fit=crop', - location: 'New York, NY', - checkIn: '2025-06-15', - checkOut: '2025-06-20', - guests: 2, - totalAmount: 1250, - status: 'confirmed', - bookingDate: '2025-05-28', - propertyId: '1', - escrowAddress: 'GCO2IP3MJNUOKS4PUDI4C7LGGMQDJGXG3COYX3WSB4HHNAHKYV5YL3VC', - canCancel: true, - canReview: false, - }, - { - id: '2', - propertyTitle: 'Cozy Beach House', - propertyImage: - 'https://images.unsplash.com/photo-1564013799919-ab600027ffc6?w=800&auto=format&fit=crop', - location: 'Miami, FL', - checkIn: '2025-07-10', - checkOut: '2025-07-15', - guests: 4, - totalAmount: 900, - status: 'pending', - bookingDate: '2025-05-26', - propertyId: '2', - canCancel: true, - canReview: false, - }, - { - id: '3', - propertyTitle: 'Mountain Cabin Retreat', - propertyImage: - 'https://images.unsplash.com/photo-1449824913935-59a10b8d2000?w=800&auto=format&fit=crop', - location: 'Aspen, CO', - checkIn: '2025-05-20', - checkOut: '2025-05-25', - guests: 3, - totalAmount: 1600, - status: 'completed', - bookingDate: '2025-04-15', - propertyId: '3', - canCancel: false, - canReview: true, - }, -]; - -const mockUser: UserProfile = { - id: '1', - name: 'Sarah Johnson', - email: 'sarah.johnson@example.com', - avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=100', - phone: '+1 (555) 123-4567', - location: 'San Francisco, CA', - bio: 'Travel enthusiast and adventure seeker. Love exploring new places and meeting interesting people.', - memberSince: '2023', - totalBookings: 12, - totalSpent: 8500, - preferences: { - notifications: true, - emailUpdates: true, - pushNotifications: false, - }, -}; - const mockTransactions = [ { id: 1, @@ -176,21 +80,54 @@ const TenantDashboard = () => { const router = useRouter(); const { logout } = useAuth(); const [activeTab, setActiveTab] = useState('bookings'); - const [bookings, setBookings] = useState(mockBookings); - const [user, setUser] = useState(mockUser); const [transactions, _setTransactions] = useState(mockTransactions); - type NotificationItem = { + const [walletBalance, _setWalletBalance] = useState(2500); + + const { + bookings: apiBookings, + user: apiUser, + isLoadingBookings, + isLoadingProfile, + bookingsError, + profileError, + refetchAll, + cancelBooking: apiCancelBooking, + updateProfile: apiUpdateProfile, + uploadAvatar: apiUploadAvatar, + deleteAccount: apiDeleteAccount, + } = useDashboard({ userId: 'tenant-1', userType: 'tenant' }); + + interface LocalNotification { id: string; + type: 'booking' | 'payment' | 'review' | 'system' | 'message' | 'reminder'; title: string; message: string; + timestamp: Date; read: boolean; - createdAt: string; - }; + priority: 'low' | 'medium' | 'high'; + actionUrl?: string; + actionText?: string; + } - const [notifications, setNotifications] = useState([]); + const [notifications, setNotifications] = useState([]); const [unreadNotifications, setUnreadNotifications] = useState(0); - const [walletBalance, _setWalletBalance] = useState(2500); - const [isLoading, setIsLoading] = useState(false); + + const user = apiUser || null; + const bookings: Booking[] = apiBookings.map((b) => ({ + id: b.id, + propertyTitle: b.propertyTitle, + propertyImage: b.propertyImage, + location: b.propertyLocation, + checkIn: b.checkIn, + checkOut: b.checkOut, + guests: b.guests, + totalAmount: b.totalAmount, + status: b.status as 'pending' | 'confirmed' | 'completed' | 'cancelled', + bookingDate: b.bookingDate, + propertyId: '1', + canCancel: true, + canReview: b.status === 'completed', + })); const stats = { totalBookings: bookings.length, @@ -198,9 +135,9 @@ const TenantDashboard = () => { (b) => b.status === 'confirmed' && new Date(b.checkIn) > new Date() ).length, completedBookings: bookings.filter((b) => b.status === 'completed').length, - totalSpent: user.totalSpent, + totalSpent: user?.totalSpent || 0, averageRating: 4.8, - memberSince: user.memberSince, + memberSince: user?.memberSince || '2023', }; const handleMarkAsRead = (id: string) => { @@ -227,89 +164,78 @@ const TenantDashboard = () => { setUnreadNotifications(0); }; + const handleRefresh = async () => { + await refetchAll(); + }; + const handleCancelBooking = async (bookingId: string) => { - setIsLoading(true); try { - // Simulate API call - await new Promise((resolve) => setTimeout(resolve, 1000)); - - setBookings((prev) => - prev.map((booking) => - booking.id === bookingId - ? { ...booking, status: 'cancelled' as const, canCancel: false } - : booking - ) - ); - + await apiCancelBooking(bookingId); const newNotification = { id: Date.now().toString(), - type: 'booking', + type: 'booking' as const, title: 'Booking Cancelled', message: 'Your booking has been successfully cancelled', timestamp: new Date(), read: false, priority: 'medium' as const, }; - setNotifications((prev) => [newNotification, ...prev]); setUnreadNotifications((prev) => prev + 1); } catch (error) { console.error('Failed to cancel booking:', error); - } finally { - setIsLoading(false); } }; - const handleUpdateProfile = async (updatedProfile: Partial) => { - setIsLoading(true); + const handleUpdateProfile = async (updates: Partial) => { try { - await new Promise((resolve) => setTimeout(resolve, 1000)); - - setUser((prev) => ({ ...prev, ...updatedProfile })); - + if (!apiUser) return; + const safeUpdates = { + ...updates, + preferences: updates.preferences + ? { + currency: apiUser.preferences?.currency || 'USD', + language: apiUser.preferences?.language || 'en', + notifications: updates.preferences.notifications, + emailNotifications: updates.preferences.emailUpdates, + marketingEmails: apiUser.preferences?.marketingEmails, + } + : apiUser.preferences, + }; + await apiUpdateProfile({ ...apiUser, ...safeUpdates } as unknown as ApiUserProfile); const newNotification = { id: Date.now().toString(), - type: 'system', + type: 'system' as const, title: 'Profile Updated', message: 'Your profile has been successfully updated', timestamp: new Date(), read: false, priority: 'low' as const, }; - setNotifications((prev) => [newNotification, ...prev]); setUnreadNotifications((prev) => prev + 1); } catch (error) { console.error('Failed to update profile:', error); - } finally { - setIsLoading(false); } }; const handleUploadAvatar = async (file: File) => { - setIsLoading(true); try { - await new Promise((resolve) => setTimeout(resolve, 2000)); - - const avatarUrl = URL.createObjectURL(file); - setUser((prev) => ({ ...prev, avatar: avatarUrl })); - + if (!user) return; + await apiUploadAvatar(user.id.toString(), file); const newNotification = { id: Date.now().toString(), - type: 'system', + type: 'system' as const, title: 'Avatar Updated', message: 'Your profile picture has been successfully updated', timestamp: new Date(), read: false, priority: 'low' as const, }; - setNotifications((prev) => [newNotification, ...prev]); setUnreadNotifications((prev) => prev + 1); } catch (error) { console.error('Failed to upload avatar:', error); - } finally { - setIsLoading(false); } }; @@ -318,14 +244,25 @@ const TenantDashboard = () => {
-

Tenant Dashboard

+ {(isLoadingBookings || isLoadingProfile) && ( + + )}
+ { -
- {user.name} - - {user.name} - -
- + {user && ( +
+
+ {user.name} + + {user.name} + +
+ +
+ )}
@@ -406,67 +347,86 @@ const TenantDashboard = () => {

-
-
-
-
-

- Total Bookings -

-

- {stats.totalBookings} -

-
-
- + {bookingsError ? ( + + ) : ( + <> +
+
+
+
+

+ Total Bookings +

+

+ {stats.totalBookings} +

+
+
+ +
+
-
-
-
-
-
-

Upcoming

-

{stats.upcomingBookings}

-
-
- +
+
+
+

+ Upcoming +

+

+ {stats.upcomingBookings} +

+
+
+ +
+
-
-
-
-
-
-

Completed

-

{stats.completedBookings}

-
-
- +
+
+
+

+ Completed +

+

+ {stats.completedBookings} +

+
+
+ +
+
-
-
-
-
-
-

- Total Spent -

-

${stats.totalSpent}

-
-
- +
+
+
+

+ Total Spent +

+

${stats.totalSpent}

+
+
+ +
+
-
-
- + + + )}
)} @@ -626,14 +586,37 @@ const TenantDashboard = () => {
)} - {activeTab === 'profile' && ( - - )} + {activeTab === 'profile' && + (profileError ? ( + + ) : user ? ( + { + if (user) await apiDeleteAccount(user.id.toString()); + }} + isLoading={isLoadingProfile} + /> + ) : ( +
+ +
+ ))} {activeTab === 'analytics' && (
@@ -725,4 +708,4 @@ const TenantDashboard = () => { ); }; -export default TenantDashboard; +export default TenantDashboard; \ No newline at end of file diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx index 806c97d3..6cebba9c 100644 --- a/apps/web/src/app/page.tsx +++ b/apps/web/src/app/page.tsx @@ -1,11 +1,14 @@ +'use client'; + import { SearchBar } from '@/components/features/search/SearchBar'; import { RightSidebar } from '@/components/layout/RightSidebar'; import PropertyGrid from '@/components/search/PropertyGrid'; +import { useProperties } from '@/hooks/useProperties'; // Keep this import import { House } from 'lucide-react'; -import Image from 'next/image'; -import { Suspense } from 'react'; export default function Home() { + const { properties, isLoading, error, refresh } = useProperties(); + return (
@@ -19,19 +22,20 @@ export default function Home() {
- Showing 23 properties + Showing {properties.length} properties
- Loading properties...
} - > - - +
); -} +} \ No newline at end of file diff --git a/apps/web/src/app/search/page.tsx b/apps/web/src/app/search/page.tsx index b572d27c..a144dfa9 100644 --- a/apps/web/src/app/search/page.tsx +++ b/apps/web/src/app/search/page.tsx @@ -19,6 +19,7 @@ export default function SearchPage() { const pageSize = 3; const [sort, setSort] = useState('price_asc'); const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); const [filters, setFilters] = useState({ price: 0, amenities: {} as Record, @@ -110,8 +111,16 @@ export default function SearchPage() {
- - {isLoading &&

Loading more properties...

} + { + setError(null); + loadNextPage(); + }} + />
diff --git a/apps/web/src/app/tenant-dashboard/components/profile-management.tsx b/apps/web/src/app/tenant-dashboard/components/profile-management.tsx deleted file mode 100644 index bb23d4ef..00000000 --- a/apps/web/src/app/tenant-dashboard/components/profile-management.tsx +++ /dev/null @@ -1,186 +0,0 @@ -import type { LegacyUserProfile as UserProfile } from '@/types'; -import Image from 'next/image'; -import type React from 'react'; -import { useState } from 'react'; - -interface ProfileManagementProps { - user: UserProfile; - onUpdateUser: (user: UserProfile) => void; -} - -const ProfileManagement: React.FC = ({ user, onUpdateUser }) => { - const [isEditing, setIsEditing] = useState(false); - const [editedUser, setEditedUser] = useState(user); - - const handleSaveProfile = () => { - const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; - if (!emailRegex.test(editedUser.email)) { - alert('Please enter a valid email address'); - return; - } - const phoneRegex = /^[\d\s\-\+\(\)]+$/; - if (editedUser.phone && !phoneRegex.test(editedUser.phone)) { - alert('Please enter a valid phone number'); - return; - } - onUpdateUser(editedUser); - setIsEditing(false); - }; - - const handleCancel = () => { - setEditedUser(user); - setIsEditing(false); - }; - - return ( -
-
-

Profile Settings

-

Manage your account information

-
- -
-
-
- {editedUser.name} -
-

- {editedUser.name} -

-

{editedUser.email}

-

- Member since {editedUser.memberSince} -

- {editedUser.verified && ( - - Verified - - )} -
- -
- -
-
- - setEditedUser((prev) => ({ ...prev, name: e.target.value }))} - className="w-full px-3 py-2 dark:text-white bg-transparent border border-gray-300 rounded-lg focus:ring-0 focus:ring-blue-500 focus:border-transparent disabled:opacity-60 disabled:cursor-not-allowed" - /> -
-
- - setEditedUser((prev) => ({ ...prev, email: e.target.value }))} - className="w-full px-3 py-2 dark:text-white border border-gray-300 rounded-lg focus:ring-0 bg-transparent focus:ring-blue-500 focus:border-transparent disabled:opacity-60 disabled:cursor-not-allowed" - /> -
-
- - setEditedUser((prev) => ({ ...prev, phone: e.target.value }))} - placeholder="+1 (555) 123-4567" - className="w-full px-3 py-2 border dark:text-white border-gray-300 rounded-lg focus:ring-0 bg-transparent focus:ring-blue-500 focus:border-transparent disabled:opacity-60 disabled:cursor-not-allowed" - /> -
-
- - setEditedUser((prev) => ({ ...prev, location: e.target.value }))} - placeholder="City, State" - className="w-full px-3 py-2 dark:text-white border border-gray-300 rounded-lg focus:ring-0 bg-transparent focus:ring-blue-500 focus:border-transparent disabled:opacity-60 disabled:cursor-not-allowed" - /> -
-
- -
- -