diff --git a/index.html b/index.html index 000821d..daa01f8 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - CityCards Travel 22-8-2025 + CityCards Customer-web diff --git a/package-lock.json b/package-lock.json index 13d84a8..071ee2a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -54,7 +54,7 @@ "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.9.4", "recharts": "^2.15.2", - "sonner": "^2.0.3", + "sonner": "^2.0.7", "tailwind-merge": "*", "tailwindcss": "^4.1.14", "vaul": "^1.1.2" diff --git a/package.json b/package.json index 2e11959..078328a 100644 --- a/package.json +++ b/package.json @@ -49,7 +49,7 @@ "react-resizable-panels": "^2.1.7", "react-router-dom": "^7.9.4", "recharts": "^2.15.2", - "sonner": "^2.0.3", + "sonner": "^2.0.7", "tailwind-merge": "*", "tailwindcss": "^4.1.14", "vaul": "^1.1.2" diff --git a/src/AppRouter.tsx b/src/AppRouter.tsx index 1a3c767..28f26f5 100644 --- a/src/AppRouter.tsx +++ b/src/AppRouter.tsx @@ -2,38 +2,36 @@ import { Routes, Route, useParams, useLocation, useNavigate } from 'react-router import { motion, AnimatePresence } from 'motion/react'; // Import all your pages -import { LoginModal } from './components/LoginModal'; -import { MelbournePage } from './components/MelbournePage'; -import { PassesPage } from './components/PassesPage'; -import { AttractionsPage } from './components/AttractionsPage'; -import { AttractionDetailsPage } from './components/AttractionDetailsPage'; -import { CheckoutPage } from './components/CheckoutPage'; -import { SecureCheckoutPage } from './components/SecureCheckoutPage'; -import { BlogsPage } from './components/BlogsPage'; -import { BlogDetailsPage } from './components/BlogDetailsPage'; +import { MelbournePage } from './pages/MelbournePage'; +import { PassesPage } from './pages/PassesPage'; +import { AttractionsPage } from './pages/AttractionsPage'; +import { AttractionDetailsPage } from './pages/AttractionDetailsPage'; +import { CheckoutPage } from './pages/CheckoutPage'; +import { SecureCheckoutPage } from './pages/SecureCheckoutPage'; +import { BlogsPage } from './pages/BlogsPage'; +import { BlogDetailsPage } from './pages/BlogDetailsPage'; import { HowItWorksPage } from './components/HowItWorksPage'; import { FAQPage } from './components/FAQPage'; -import { PrivacyPolicyPage } from './components/PrivacyPolicyPage'; -import { AboutUsPage } from './components/AboutUsPage'; -import { ProfilePage } from './components/ProfilePage'; -import { CreateMagicItineraryPage } from './components/CreateMagicItineraryPage'; -import { ItineraryViewPage } from './components/ItineraryViewPage'; -import { OffersPage } from './components/OffersPage'; -import { CityCardsPage } from './components/CityCardsPage'; -import { MagicItineraryPage } from './components/MagicItineraryPage'; -import { PostCardsPage } from './components/PostCardsPage'; -import { DownloadAppPage } from './components/DownloadAppPage'; -import { EsimsPage } from './components/EsimsPage'; -import { HotelDiscountsPage } from './components/HotelDiscountsPage'; -import { ContactUsPage } from './components/ContactUsPage'; +import { PrivacyPolicyPage } from './pages/PrivacyPolicyPage'; +import { AboutUsPage } from './pages/AboutUsPage'; +import { ProfilePage } from './pages/ProfilePage'; +import { CreateMagicItineraryPage } from './pages/CreateMagicItineraryPage'; +import { ItineraryViewPage } from './pages/ItineraryViewPage'; +import { OffersPage } from './pages/OffersPage'; +import { CityCardsPage } from './pages/CityCardsPage'; +import { MagicItineraryPage } from './pages/MagicItineraryPage'; +import { PostCardsPage } from './pages/PostCardsPage'; +import { DownloadAppPage } from './pages/DownloadAppPage'; +import { HotelDiscountsPage } from './pages/HotelDiscountsPage'; +import { ContactUsPage } from './pages/ContactUsPage'; import { pageTransition } from './utils/animations'; import { LandingPage } from './pages/landingPage'; import ComingSoonPage from './pages/ComingSoonPage'; -import { SuperSavingsPage } from './components/SuperSavingsPage'; -import { WhatsIncluded } from './components/WhatsIncluded'; -import { LandingMagicItineraryPage } from './components/LandingMagicItineraryPage'; -import { DiscoverPage } from './components/DiscoverPage'; +import { SuperSavingsPage } from './pages/SuperSavingsPage'; +import { WhatsIncluded } from './pages/WhatsIncluded'; +import { LandingMagicItineraryPage } from './pages/LandingMagicItineraryPage'; +import { DiscoverPage } from './pages/DiscoverPage'; // User type definition interface User { diff --git a/src/Redux/Store.tsx b/src/Redux/Store.tsx index 7d03f11..ecb7f5d 100644 --- a/src/Redux/Store.tsx +++ b/src/Redux/Store.tsx @@ -1,23 +1,24 @@ import { configureStore } from "@reduxjs/toolkit"; -import { fakeApi } from "./services/fakeApi.service"; import { attractionsApi } from "./services/attractions.service"; import { citiesApi } from "./services/cities.service"; +import { authApi } from "./services/auth.service"; +import { profileApi } from "./services/profile.service"; export const store = configureStore({ reducer: { - [fakeApi.reducerPath]:fakeApi.reducer, - [attractionsApi.reducerPath]:attractionsApi.reducer, - [citiesApi.reducerPath]:citiesApi.reducer + [attractionsApi.reducerPath]: attractionsApi.reducer, + [citiesApi.reducerPath]: citiesApi.reducer, + [authApi.reducerPath]: authApi.reducer, + [profileApi.reducerPath]: profileApi.reducer }, - middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat( - -fakeApi.middleware, -attractionsApi.middleware, -citiesApi.middleware + attractionsApi.middleware, + citiesApi.middleware, + authApi.middleware, + profileApi.middleware ), }); export type RootState = ReturnType; diff --git a/src/Redux/baseQuery.ts b/src/Redux/baseQuery.ts index 37c41cc..36c9339 100644 --- a/src/Redux/baseQuery.ts +++ b/src/Redux/baseQuery.ts @@ -8,10 +8,9 @@ export const baseQuery = fetchBaseQuery({ const token = localStorage.getItem("accessToken"); if (token) { headers.set("Authorization", `Bearer ${token}`); - // headers.set("access-token", token); + headers.set("access-token", token); } // headers.set("Content-Type", "application/json"); return headers; }, -}); - +}); \ No newline at end of file diff --git a/src/Redux/services/auth.service.ts b/src/Redux/services/auth.service.ts new file mode 100644 index 0000000..100cd07 --- /dev/null +++ b/src/Redux/services/auth.service.ts @@ -0,0 +1,53 @@ + +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const authApi = createApi({ + reducerPath: "authApi", + baseQuery: baseQuery, + + tagTypes: ["profile", "Transaction"], + + + endpoints: (builder) => ({ + // Login + + login: builder.mutation({ + query: (credentials) => ({ + url: "/website/send-otp", + method: "POST", + body: credentials, + }), + }), + + verifyOtp: builder.mutation({ + query: (credentials) => ({ + url: "/website/user/verify-otp", + method: "POST", + body: credentials, + }), + }), + + register: builder.mutation({ + query: (credentials) => ({ + url: "/website/user/register", + method: "POST", + body: credentials, + }), + }), + + logoutUser: builder.mutation({ + query: () => ({ + url: "/website/user/logout", + method: "POST" + }) + }) + }), +}); + +export const { + useLoginMutation, + useVerifyOtpMutation, + useRegisterMutation, + useLogoutUserMutation +} = authApi; \ No newline at end of file diff --git a/src/Redux/services/fakeApi.service.ts b/src/Redux/services/fakeApi.service.ts deleted file mode 100644 index b5f90ae..0000000 --- a/src/Redux/services/fakeApi.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react' - -export const fakeApi = createApi({ - reducerPath: 'fakeApi', - baseQuery: fetchBaseQuery({ - baseUrl: " https://fakestoreapi.com", - - }), - endpoints: (builder) => ({ - getProducts: builder.query({ - query: () => ({ - url: 'products', - method: 'GET', - }), - }), - }), -}) - -export const { useGetProductsQuery} = fakeApi diff --git a/src/Redux/services/profile.service.ts b/src/Redux/services/profile.service.ts new file mode 100644 index 0000000..afa50f0 --- /dev/null +++ b/src/Redux/services/profile.service.ts @@ -0,0 +1,45 @@ + +import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"; +import { baseQuery } from "../baseQuery"; + +export const profileApi = createApi({ + reducerPath: "profileApi", + baseQuery, + + tagTypes: ["userDetails"], + + endpoints: (builder) => ({ + + getUserProfileDetails: builder.query({ + query: (id) => `/website/user/${id}`, + providesTags: ["userDetails"] + }), + + updateUserProfileDetails: builder.mutation({ + query: ({ userDetails, userId }) => ({ // keep the name of the variables being passed here same as when calling the mutation hook + url: `/website/user/${userId}`, + method: "PUT", + body: userDetails + }), + invalidatesTags: ["userDetails"] + }), + + getUserPasses: builder.query({ + query: ({ cardMode, sort }) => { + const params = new URLSearchParams() + + if(cardMode) params.append('cardMode',cardMode); + if(sort) params.append('sort',sort); + + return `/website/passes/all?${params.toString()}` + } + }) + + }) +}); + +export const { + useGetUserProfileDetailsQuery, + useUpdateUserProfileDetailsMutation, + useGetUserPassesQuery +} = profileApi; \ No newline at end of file diff --git a/src/imports/AfterLogin.tsx b/src/components/AfterLogin.tsx similarity index 100% rename from src/imports/AfterLogin.tsx rename to src/components/AfterLogin.tsx diff --git a/src/imports/BeforeLogin.tsx b/src/components/BeforeLogin.tsx similarity index 100% rename from src/imports/BeforeLogin.tsx rename to src/components/BeforeLogin.tsx diff --git a/src/components/CTAButton.tsx b/src/components/CTAButton.tsx index 9b80d9f..0c16431 100644 --- a/src/components/CTAButton.tsx +++ b/src/components/CTAButton.tsx @@ -1,6 +1,6 @@ import { motion } from 'motion/react'; -import BeforeLogin from '../imports/BeforeLogin'; -import AfterLogin from '../imports/AfterLogin'; +import BeforeLogin from './BeforeLogin'; +import AfterLogin from './AfterLogin'; interface User { email: string; diff --git a/src/components/CitySelectionDialog.tsx b/src/components/CitySelectionDialog.tsx index 203aa72..3ede930 100644 --- a/src/components/CitySelectionDialog.tsx +++ b/src/components/CitySelectionDialog.tsx @@ -31,7 +31,14 @@ export function CitySelectionDialog({ const { data: cities, isLoading } = useGetCityListWithBannerQuery({ search }) if (isLoading) { - return
Loading...
+ return ( +
+
+
+

Loading...

+
+
+ ); } @@ -40,7 +47,7 @@ export function CitySelectionDialog({ // ✅ Call the onCitySelect callback if provided (passing cityId) if (onCitySelect) { - onCitySelect(String(city.id)); + onCitySelect(String(city.cityName)); } else { // ✅ Default behavior: navigate to passes page navigate(`/passes?city=${encodeURIComponent(city.cityName)}`); diff --git a/src/components/LandingMagicItinerary.tsx b/src/components/LandingMagicItinerary.tsx index a67a853..8ce3422 100644 --- a/src/components/LandingMagicItinerary.tsx +++ b/src/components/LandingMagicItinerary.tsx @@ -31,7 +31,7 @@ export function LandingMagicItinerary() { }; return ( -
+
{/* Dynamic Background */}
{/* Background Image as fallback */} diff --git a/src/components/LandingUpcomingCities.tsx b/src/components/LandingUpcomingCities.tsx index 2d724a0..cd15393 100644 --- a/src/components/LandingUpcomingCities.tsx +++ b/src/components/LandingUpcomingCities.tsx @@ -113,8 +113,15 @@ export function LandingUpcomingCities() { const { data, isLoading } = useGetUpcomingCitiesQuery(listType) - if(isLoading){ - return
Loading...
+ if (isLoading) { + return ( +
+
+
+

Loading...

+
+
+ ); } const handleMouseDown = (e: React.MouseEvent) => { diff --git a/src/components/LandingVarietyOfAdventures.tsx b/src/components/LandingVarietyOfAdventures.tsx index 3e7ab94..4af0519 100644 --- a/src/components/LandingVarietyOfAdventures.tsx +++ b/src/components/LandingVarietyOfAdventures.tsx @@ -154,7 +154,7 @@ export function LandingVarietyOfAdventures() {
{/* Header */}
- Experience{' '} for Every Traveller - - From iconic laneways and world-class coffee to stunning gardens and vibrant markets, + From iconic laneways and world-class coffee to stunning gardens and vibrant markets, discover Melbourne's unique character through curated experiences that showcase the city's soul.
@@ -180,7 +180,7 @@ export function LandingVarietyOfAdventures() { {/* Carousel Container - Full Width */}
{/* Scrolling Track */} -
- + {/* Bottom Content Card */}
-
- + {/* Icon */} -
- + {/* Attraction Info */}
@@ -332,19 +332,19 @@ export function LandingVarietyOfAdventures() { {/* Gradient Fade Edges */} -
-
+
+
{/* CTA Button */} - - + +
+
+ -

- Login -

-

- Enter your email Id and verify with OTP sent on it. -

-
+

+ Login +

+

+ Enter your email and verify with OTP +

+
- {/* Content */} -
- {step === 'email' ? ( -
- {/* Email Input */} -
- - setEmail(e.target.value)} - className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" - onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()} - /> - {helperText && ( -

- {helperText} -

- )} -
- - {/* Send OTP Button */} - -
- ) : ( -
- {/* Email Display */} -
- -
- {email} -
- {helperText && ( -

- {helperText} -

- )} -
- - {/* OTP Input */} -
- -
- {otp.map((digit, index) => ( - handleOTPChange(index, e.target.value.replace(/\D/g, ''))} - onKeyDown={(e) => handleOTPKeyDown(index, e)} - data-otp-index={index} - className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-300 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-primary focus:outline-none transition-colors" - /> - ))} +
+ {step === 'email' ? ( + // ... Email step (unchanged) +
+
+ + setEmail(e.target.value)} + onKeyDown={(e) => e.key === 'Enter' && handleSendOTP()} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> + {error &&

{error}

} + {helperText &&

{helperText}

}
- {/* Countdown */} - {countdown > 0 && ( -

- {formatCountdown(countdown)} -

- )} -
- - {/* Verify Button */} - - - {/* Resend OTP */} - {countdown === 0 && ( - - )} -
- )} + {isSendingOtp ? 'Sending OTP...' : 'Send OTP'} + +
+ +
+
+ ) : ( +
+ {/* Email Display */} +
+ +
+ {email} +
+
+ + {/* OTP Inputs with Paste Support */} +
+ +
+ {otp.map((digit, index) => ( + handleOTPChange(index, e.target.value.replace(/\D/g, ''))} + onKeyDown={(e) => handleOTPKeyDown(index, e)} + onPaste={handlePaste} // ← Paste support added here + data-otp-index={index} + className="w-12 h-12 text-center font-poppins font-semibold text-lg bg-gray-300 border-0 rounded-xl focus:bg-white focus:ring-2 focus:ring-gray-800 focus:outline-none transition-all" + /> + ))} +
+ + {countdown > 0 && ( +

+ Resend OTP in {formatCountdown(countdown)} +

+ )} +
+ + {error &&

{error}

} + + + + {countdown === 0 && ( + + )} +
+ )} +
-
-
- - )} - +
+ + ) + } + + setShowRegisterModal(false)} + onLoginClick={() => { + setShowRegisterModal(false); + setStep('email'); + setEmail(''); + }} + /> + ); } \ No newline at end of file diff --git a/src/components/Navbar.tsx b/src/components/Navbar.tsx index 0dcef92..ca6e863 100644 --- a/src/components/Navbar.tsx +++ b/src/components/Navbar.tsx @@ -143,11 +143,11 @@ export default function Navbar({ isShared: false }, { - label: 'Your PostCard', + label: 'Your Postcard', path: '/postcards', isShared: true, - landingLabel: 'Your PostCard', - melbourneLabel: 'Your PostCard' + landingLabel: 'Your Postcard', + melbourneLabel: 'Your Postcard' } ], melbourne: [ @@ -186,11 +186,11 @@ export default function Navbar({ melbourneLabel: 'Your Card' }, { - label: 'Your PostCard', + label: 'Your Postcard', path: '/postcards', isShared: true, - landingLabel: 'Your PostCard', - melbourneLabel: 'Your PostCard' + landingLabel: 'Your Postcard', + melbourneLabel: 'Your Postcard' } ] }; @@ -299,11 +299,11 @@ export default function Navbar({ setDialogSource('navbar'); }; - const handleCitySelectFromNavbar = (cityId: string) => { - console.log('City selected from navbar:', cityId); - onCityChange(cityId); + const handleCitySelectFromNavbar = (cityName: string) => { + console.log('City selected from navbar:', cityName); + onCityChange(cityName); - if (cityId.toLowerCase() === '1') { + if (cityName.toLowerCase() === 'melbourne') { setNavigationSource('melbourne'); navigate('/melbourne'); } else { @@ -334,11 +334,11 @@ export default function Navbar({ handleCloseCityDialog(); }; - const handleCitySelect = (cityId: string) => { + const handleCitySelect = (cityName: string) => { if (dialogSource === 'cta') { - handleCitySelectFromCTA(cityId); + handleCitySelectFromCTA(cityName); } else { - handleCitySelectFromNavbar(cityId); + handleCitySelectFromNavbar(cityName); } }; @@ -533,7 +533,7 @@ export default function Navbar({ <> {/* Desktop Navbar - Enhanced Glassmorphism */} void; + onLoginClick: () => void; +} + +export function RegisterModal({ isOpen, onClose, onLoginClick }: RegisterModalProps) { + const [formData, setFormData] = useState({ + firstName: '', + lastName: '', + emailAddress: '', + isdCode: '+91', + mobileNumber: '', + address1: '', + address2: '', + city: '', + state: '', + country: 'Australia', + postalCode: '' + }); + const [helperText, setHelperText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + const [register, { isLoading: isRegistering }] = useRegisterMutation(); + + useEffect(() => { + if (!isOpen) { + setFormData({ + firstName: '', + lastName: '', + emailAddress: '', + isdCode: '+91', + mobileNumber: '', + address1: '', + address2: '', + city: '', + state: '', + country: 'Australia', + postalCode: '' + }); + setHelperText(''); + } + }, [isOpen]); + + const handleInputChange = (field: string, value: string) => { + setFormData(prev => ({ ...prev, [field]: value })); + }; + + const validateForm = () => { + if (!formData.firstName.trim()) { + toast.error('First name is required'); + return false; + } + if (!formData.lastName.trim()) { + toast.error('Last name is required'); + return false; + } + if (!formData.emailAddress.trim() || !formData.emailAddress.includes('@')) { + toast.error('Please enter a valid email address'); + return false; + } + if (!formData.mobileNumber.trim()) { + toast.error('Mobile number is required'); + return false; + } + if (!/^\d+$/.test(formData.mobileNumber.trim())) { + toast.error('Mobile number must contain only digits'); + return false; + } + if (!formData.address1.trim()) { + toast.error('Address is required'); + return false; + } + if (!formData.city.trim()) { + toast.error('City is required'); + return false; + } + if (!formData.state.trim()) { + toast.error('State is required'); + return false; + } + if (!formData.postalCode.trim()) { + toast.error('Postal code is required'); + return false; + } + if (!/^\d+$/.test(formData.postalCode.trim())) { + toast.error('Postal code must contain only digits'); + return false; + } + return true; + }; + + + const handleRegister = async () => { + if (!validateForm()) { + return; + } + + setHelperText(''); + setIsLoading(true); + + try { + const response = await register(formData).unwrap(); + console.log('Registration response:', response); + + toast.success('Registration successful! Please login.'); + setTimeout(() => { + onLoginClick(); + onClose(); + }, 2000); + } catch (error: any) { + console.error('Registration error:', error); + const errorMessage = error?.data?.message || 'Registration failed. Please try again.'; + toast.error(errorMessage); + setHelperText(errorMessage); + } finally { + setIsLoading(false); + } + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + e.preventDefault(); + handleRegister(); + } + }; + + return ( + + {isOpen && ( + <> + + + +
+
+ + +

+ Create Account +

+

+ Register to get started with City Cards +

+
+ +
+
+ {/* Personal Information */} +
+

Personal Information

+
+
+ + handleInputChange('firstName', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+ + handleInputChange('lastName', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+ +
+ + handleInputChange('emailAddress', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+
+ + +
+ +
+ + handleInputChange('mobileNumber', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+
+ + {/* Address Information */} +
+

Address Information

+ +
+ + handleInputChange('address1', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+ + handleInputChange('address2', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+
+ + handleInputChange('city', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+ +
+ + handleInputChange('state', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+ +
+
+ + +
+ +
+ + handleInputChange('postalCode', e.target.value)} + className="font-poppins text-base h-12 bg-gray-50 border-0 rounded-xl placeholder:text-gray-400" + /> +
+
+
+ + {helperText && ( +

+ {helperText} +

+ )} + + + +
+ +
+
+
+
+
+ + )} +
+ ); +} diff --git a/src/context/AuthContext.tsx b/src/context/AuthContext.tsx index 8cabeb8..50dba2a 100644 --- a/src/context/AuthContext.tsx +++ b/src/context/AuthContext.tsx @@ -3,7 +3,9 @@ import { useNavigate } from 'react-router-dom'; interface User { email: string; - name: string + name: string; + accessToken:string; + userId:string; } interface AuthContextType { @@ -29,11 +31,15 @@ export const AuthProvider = ({ children }: { children: React.ReactNode }) => { const login = (userData: User) => { setUser(userData) localStorage.setItem("user", JSON.stringify(userData)) + localStorage.setItem("accessToken", userData?.accessToken) + localStorage.setItem("userId", userData?.userId) } const logout = () => { setUser(null) localStorage.removeItem("user") + localStorage.removeItem("accessToken") + localStorage.removeItem("userId") navigate("/") } diff --git a/src/global.d.ts b/src/global.d.ts index 903f3f8..eb5a13c 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -32,3 +32,5 @@ declare module '*.mp4' { const src: string; export default src; } + +declare module "*.css"; diff --git a/src/guidelines/Guidelines.md b/src/guidelines/Guidelines.md deleted file mode 100644 index 4a00ca3..0000000 --- a/src/guidelines/Guidelines.md +++ /dev/null @@ -1,345 +0,0 @@ -# Image Reference Guidelines - -**IMPORTANT**: When I provide an image for reference, it is for design reference only. Do NOT use that image inside any image section in the code. The image is provided to help understand the layout, styling, and visual direction - not to be embedded as an actual image in the application. - -# CityCards Typography Guidelines - -**Project**: Typography Guidelines for CityCards Travel Website - -## Font System - -### Primary Font -- **Poppins**: Used for all text including headings (H1–H6), body text, buttons, labels, and forms - Clean, readable, and consistent throughout - -### Font Weight Scale & Usage -- **font-light (300)**: Hero headings only - For creating dynamic contrast in H1/H2 -- **font-normal (400)**: Standard body text - Default weight for paragraphs -- **font-medium (500)**: Buttons, navigation links, subtle emphasis -- **font-semibold (600)**: Section headings, primary buttons -- **font-bold (700)**: Hero keywords, strong emphasis - -## Heading Typography Specifications - -### H1 - Hero/Main Page Headings -- **Font**: Poppins -- **Size**: `text-5xl md:text-6xl lg:text-7xl` (48px/60px/72px - targeting ~64px) -- **Line Height**: `leading-tight` -- **Pattern**: Dynamic multi-weight with gradient/italic accents (max 2 emphasis styles) - -```jsx -

- Discover{' '} - - Melbourne's - {' '} - Best Experiences -

-``` - -### H2 - Section Headings -- **Font**: Poppins -- **Size**: `text-2xl md:text-3xl lg:text-4xl` (24px/36px/48px) -- **Line Height**: `leading-tight` -- **Pattern**: Mixed weights with gradient emphasis - -```jsx -

- Explore{' '} - - Amazing - {' '} - Cities -

-``` - -### H3 - Subsection Headings -- **Font**: Poppins -- **Size**: `text-xl md:text-2xl` (24px/30px) -- **Line Height**: `leading-snug` -- **Weight**: `font-semibold` - -```jsx -

- Feature Title -

-``` - -### H4 - Component Headings -- **Font**: Poppins -- **Size**: `text-lg md:text-xl` (20px/24px) -- **Line Height**: `leading-snug` -- **Weight**: `font-medium` or `font-semibold` - -```jsx -

- Component Heading -

-``` - -### H5 - Card/Item Titles -- **Font**: Poppins -- **Size**: `text-lg` (18px) -- **Line Height**: `leading-snug` -- **Weight**: `font-medium` - -```jsx -
- Card Title -
-``` - -### H6 - Small Headings -- **Font**: Poppins -- **Size**: `text-base` (16px) -- **Line Height**: `leading-snug` -- **Weight**: `font-medium` - -```jsx -
- Small Heading -
-``` - -## Body Typography Specifications - -### Large Body Text -- **Font**: Poppins -- **Size**: `text-xl` (20px) -- **Line Height**: `leading-relaxed` -- **Weight**: `font-normal` - -```jsx -

- Large descriptive text for important sections -

-``` - -### Regular Body Text -- **Font**: Poppins -- **Size**: `text-base` (16px) -- **Line Height**: `leading-relaxed` -- **Weight**: `font-normal` - -```jsx -

- Regular body text content -

-``` - -### Small Body Text -- **Font**: Poppins -- **Size**: `text-sm` (14px) -- **Line Height**: `leading-relaxed` -- **Weight**: `font-normal` or `font-light` - -```jsx -

- Caption or meta information -

-``` - -## Interactive Element Typography - -### Buttons -- **Font**: Poppins -- **Primary Weight**: `font-semibold` -- **Secondary Weight**: `font-medium` -- **Min Size**: 16px - -```jsx -// Primary Button - - -// Secondary Button - -``` - -### Navigation Links -- **Font**: Poppins -- **Weight**: `font-medium` -- **Size**: `text-base` (16px) - -```jsx - - Navigation Link - -``` - -### Form Labels -- **Font**: Poppins -- **Weight**: `font-light` or `font-normal` -- **Size**: `text-sm` or `text-base` (14px/16px) - -```jsx - -``` - -### Form Inputs -- **Font**: Poppins -- **Weight**: `font-normal` -- **Size**: `text-base` (16px) - -```jsx - -``` - -## Accessibility Standards - -### Text Size Requirements -- **Minimum Text Size**: 14px -- **Interactive Minimum Size**: 16px -- **Contrast**: WCAG AA or higher -- **Heading Hierarchy**: Maintain semantic order (H1 → H2 → H3 etc.) - -### Implementation Requirements -```jsx -// Always include explicit font and size classes to override defaults -

- Content with explicit styling -

-``` - -## Typography Rules - -### DO ✅ -- Use Poppins for all text (headings and body) -- Apply max 2 emphasis styles per heading -- Use gradient effects sparingly -- Keep line-heights consistent -- Always specify explicit font classes to override component defaults - -### DON'T ❌ -- Don't use font-light in small text -- Don't mix more than 3 weights in one heading -- Don't go below 14px for captions -- Don't override font sizes without Tailwind classes -- Don't break semantic heading hierarchy - -## Implementation Guidelines - -### Component Styling Override -**IMPORTANT**: Always explicitly set typography classes to override component defaults: - -```jsx -// ✅ CORRECT - Explicit typography classes - - - - Card Title - - - -

- Card content with explicit styling -

-
-
- -// ❌ INCORRECT - Relying on defaults - - - Card Title - - -

Card content without explicit styling

-
-
-``` - -### Dynamic Heading Patterns -```jsx -// Pattern 1: Light → Bold (H1/H2 only) -

- Discover{' '} - - Amazing - {' '} - Destinations -

- -// Pattern 2: Normal → Semibold (H3/H4) -

- Experience{' '} - Melbourne's Culture -

-``` - -### Responsive Typography -```jsx -// Mobile-first responsive scaling -

- Responsive Heading -

- -

- Responsive body text -

-``` - -**Add your own guidelines here** - diff --git a/src/main.tsx b/src/main.tsx index 241b0b3..cfff6d7 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -4,10 +4,12 @@ import App from "./App"; import "./index.css"; import { Provider } from "react-redux"; import { store } from "./Redux/Store"; +import { Toaster } from "sonner"; createRoot(document.getElementById("root")!).render( + diff --git a/src/components/AboutUsPage.tsx b/src/pages/AboutUsPage.tsx similarity index 98% rename from src/components/AboutUsPage.tsx rename to src/pages/AboutUsPage.tsx index b68a755..29b87fd 100644 --- a/src/components/AboutUsPage.tsx +++ b/src/pages/AboutUsPage.tsx @@ -1,13 +1,13 @@ import React from 'react'; import { motion } from 'motion/react'; import { ArrowLeft, Heart, MapPin, Zap, Globe, Users, Camera, Coffee } from 'lucide-react'; -import { Button } from './ui/button'; -import { ImageWithFallback } from './figma/ImageWithFallback'; -import Navbar from './Navbar'; -import { Footer } from './Footer'; -import { MobileAppSection } from './MobileAppSection'; -import { EnhancedTestimonials } from './EnhancedTestimonials'; -import { ReviewsSection } from './ReviewsSection'; +// import { Button } from './ui/button'; +import { ImageWithFallback } from '../components/figma/ImageWithFallback'; +import Navbar from '../components/Navbar'; +import { Footer } from '../components/Footer'; +import { MobileAppSection } from '../components/MobileAppSection'; +import { EnhancedTestimonials } from '../components/EnhancedTestimonials'; +import { ReviewsSection } from '../components/ReviewsSection'; interface User { email: string; diff --git a/src/components/AttractionDetailsPage.tsx b/src/pages/AttractionDetailsPage.tsx similarity index 95% rename from src/components/AttractionDetailsPage.tsx rename to src/pages/AttractionDetailsPage.tsx index d92c3b1..34fd0d6 100644 --- a/src/components/AttractionDetailsPage.tsx +++ b/src/pages/AttractionDetailsPage.tsx @@ -1,10 +1,10 @@ import { useState } from 'react'; import { motion } from 'motion/react'; import { ArrowLeft, Clock, Users, Calendar, MapPin, Star, Check, X, ChevronLeft, ChevronRight } from 'lucide-react'; -import { Button } from './ui/button'; -import { Badge } from './ui/badge'; -import { Card, } from './ui/card'; -import { ImageWithFallback } from './figma/ImageWithFallback'; +import { Button } from '../components/ui/button'; +import { Badge } from '../components/ui/badge'; +import { Card, } from '../components/ui/card'; +import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { Layout } from '../Layout'; import { useParams } from 'react-router-dom'; import { useGetAttractionDetailsByIdQuery } from '../Redux/services/attractions.service'; @@ -31,7 +31,14 @@ export function AttractionDetailsPage({ const { data: attraction, isLoading } = useGetAttractionDetailsByIdQuery(Number(attractionId)); if (isLoading) { - return
loading...
+ return ( +
+
+
+

Loading...

+
+
+ ); } return ( @@ -40,7 +47,7 @@ export function AttractionDetailsPage({ onSignInClick={onSignInClick} onSignOutClick={onSignOutClick} user={user} - // showCitySubmenu={false} + // showCitySubmenu={false} >
{/* Back Button */} @@ -82,7 +89,7 @@ export function AttractionDetailsPage({ {attraction.title} {' '} - Day Trip by {attraction.partner.businessName} + Day Trip by {attraction.partner.businessName}
@@ -99,10 +106,10 @@ export function AttractionDetailsPage({
{/* Gallery images */} - {attraction.attractionGalleries.slice().map((image:any) => ( + {attraction.attractionGalleries.slice().map((image: any) => (
diff --git a/src/components/AttractionsPage.tsx b/src/pages/AttractionsPage.tsx similarity index 96% rename from src/components/AttractionsPage.tsx rename to src/pages/AttractionsPage.tsx index 79ec7df..4a7d35b 100644 --- a/src/components/AttractionsPage.tsx +++ b/src/pages/AttractionsPage.tsx @@ -2,12 +2,12 @@ import { useEffect, useState } from 'react'; import { motion } from 'motion/react'; import { Search, Star, Clock } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; -import { Button } from './ui/button'; -import { Input } from './ui/input'; -import { Card, CardContent } from './ui/card'; -import { Badge } from './ui/badge'; -import { Checkbox } from './ui/checkbox'; -import { ImageWithFallback } from './figma/ImageWithFallback'; +import { Button } from '../components/ui/button'; +import { Input } from '../components/ui/input'; +import { Card, CardContent } from '../components/ui/card'; +import { Badge } from '../components/ui/badge'; +import { Checkbox } from '../components/ui/checkbox'; +import { ImageWithFallback } from '../components/figma/ImageWithFallback'; import { Layout } from '../Layout'; import { useGetAttractionFiltersQuery, useGetCustomerAttractionsQuery } from '../Redux/services/attractions.service'; interface User { @@ -230,7 +230,7 @@ export function AttractionsPage({ const [selectedPassType, setSelectedPassType] = useState(null); const cityId = 1 - + const { data: filterData, isLoading } = useGetAttractionFiltersQuery(cityId) const { data: attractions } = useGetCustomerAttractionsQuery({ cityId, // required @@ -239,9 +239,16 @@ export function AttractionsPage({ cardType: selectedPassType, // optional search, // optional }); - + if (isLoading) { - return
Loading...
+ return ( +
+
+
+

Loading...

+
+
+ ); } const handleAttractionClick = (attractionId: string) => { @@ -254,7 +261,7 @@ export function AttractionsPage({ const showingFrom = 1; const showingTo = Math.min(12, attractions?.length); const totalItems = attractions?.length; - + function handlePassTypeSelection(key: string, checked: boolean) { if (checked) { setSelectedPassType(key); // only keep the newly selected one @@ -403,7 +410,7 @@ export function AttractionsPage({ htmlFor={key} className="font-poppins text-sm text-[#414141] cursor-pointer flex-1 font-normal" > - {key==="selective_pass" ?"Selective":"Unlimited"} ({count as number}) + {key === "selective_pass" ? "Selective" : "Unlimited"} ({count as number})
))} @@ -493,7 +500,7 @@ export function AttractionsPage({ @@ -384,7 +430,7 @@ export function ProfilePage({ // Determine which pass type to show const hasUnlimitedPass = activePasses.some(pass => pass.type === 'Unlimited Pass'); const hasSelectivePass = activePasses.some(pass => pass.type === 'Flexi Pass'); - + if (hasUnlimitedPass) { return ( <> @@ -394,7 +440,7 @@ export function ProfilePage({ Melbourne Unlimited Card

- Unlimited access to 25+ attractions. Visit as many places as you want with one simple card. + Unlimited access to 25+ attractions. Visit as many places as you want with one simple card. Save up to 40% compared to individual tickets.

@@ -423,13 +469,13 @@ export function ProfilePage({ {/* Purchase CTA */}
- -
@@ -478,13 +524,13 @@ export function ProfilePage({ {/* Purchase CTA */}
- -
@@ -535,13 +581,13 @@ export function ProfilePage({ {/* Purchase CTA */}
- -
- +
Attractions: @@ -616,10 +662,10 @@ export function ProfilePage({ ))}
- + {/* Offers Button */}
-
- +
Attractions visited: @@ -689,7 +735,7 @@ export function ProfilePage({ >

My Itineraries

-
- +
@@ -728,8 +774,8 @@ export function ProfilePage({
-
{/* Footer */} -