diff --git a/package-lock.json b/package-lock.json index ddbc4dc..c46fd7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,11 +1,11 @@ { - "name": "nextjs-15-social-media-app", + "name": "GoTogether", "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "nextjs-15-social-media-app", + "name": "GoTogether", "version": "0.1.0", "dependencies": { "@hookform/resolvers": "^3.9.0", diff --git a/src/app/(auth)/actions.ts b/src/app/(auth)/actions.ts index a51e24d..a9b6e09 100644 --- a/src/app/(auth)/actions.ts +++ b/src/app/(auth)/actions.ts @@ -1,25 +1,14 @@ -"use server"; - -import { cookies } from "next/headers"; -import { redirect } from "next/navigation"; - -export async function logout() { - const cookieStore = cookies(); - const accessToken = cookieStore.get("access_token")?.value; - - if (!accessToken) { - return redirect("/login"); - } - - cookieStore.set("access_token", "", { - path: "/", - maxAge: 0, - }); - cookieStore.set("user", "", { path: "/", maxAge: 0 }); - cookieStore.set("refresh_token", "", { - path: "/api/auth/refresh", - maxAge: 0, - }); - const keycloakLogoutUrl = `${process.env.NEXT_PUBLIC_KEYCLOAK_LOGOUT_URL || "http://localhost:8084/realms/kong/protocol/openid-connect/logout"}?client_id=kong-oidc&post_logout_redirect_uri=${process.env.NEXT_PUBLIC_KEYCLOAK_REDIRECT_URI || "http://localhost:3000/login"}`; - return redirect(keycloakLogoutUrl); +export function logout() { + // Clear client-side cookies + document.cookie = "access_token=; path=/; max-age=0; Secure; SameSite=Lax"; + document.cookie = "user=; path=/; max-age=0; Secure; SameSite=Lax"; + document.cookie = + "refresh_token=; path=/api/auth/refresh; max-age=0; Secure; SameSite=Lax"; + + // Redirect to Keycloak logout endpoint + const logoutUrl = process.env.NEXT_PUBLIC_KEYCLOAK_LOGOUT_URL!; + const redirectUri = process.env.NEXT_PUBLIC_KEYCLOAK_REDIRECT_URI!.trim(); + + const fullLogoutUrl = `${logoutUrl}?client_id=kong-oidc&post_logout_redirect_uri=${encodeURIComponent(redirectUri)}`; + window.location.href = fullLogoutUrl; } diff --git a/src/app/(auth)/login/GoogleSignInButton.tsx b/src/app/(auth)/login/GoogleSignInButton.tsx index 917bf53..00339c5 100644 --- a/src/app/(auth)/login/GoogleSignInButton.tsx +++ b/src/app/(auth)/login/GoogleSignInButton.tsx @@ -10,7 +10,7 @@ export default function GoogleSignInButton() { diff --git a/src/app/(auth)/login/actions.ts b/src/app/(auth)/login/actions.ts index 969051c..666aad9 100644 --- a/src/app/(auth)/login/actions.ts +++ b/src/app/(auth)/login/actions.ts @@ -46,7 +46,7 @@ export async function login( cookieStore.set("access_token", accessToken, { path: "/", - httpOnly: true, + httpOnly: false, secure: process.env.NODE_ENV === "production", maxAge: 3600, // Or use expiresIn from backend response if available and preferred }); diff --git a/src/app/(main)/page.tsx b/src/app/(main)/page.tsx index dc8f88a..191ddec 100644 --- a/src/app/(main)/page.tsx +++ b/src/app/(main)/page.tsx @@ -15,7 +15,11 @@ import { ListChecks, Loader2, // Import Loader2 } from "lucide-react"; -import { getNearbyPlacesAction, geocodeAddressAction, GeocodeResult } from "./actions"; // Import geocodeAddressAction and GeocodeResult +import { + getNearbyPlacesAction, + geocodeAddressAction, + GeocodeResult, +} from "./actions"; // Import geocodeAddressAction and GeocodeResult import type { LocationDetail } from "@/types/location-types"; import { useTranslations } from "next-intl"; import { ROUTES, RouteKey } from "@/lib/routes"; // Import ROUTES and RouteKey @@ -24,17 +28,94 @@ import HorizontalScrollBar from "@/components/home/horizontal-scroll-bar"; // Fallback image definitions, updated to include vicinity and geometry const topPicksFallbackImages = [ - { id: "tp1", name: "Beach Paradise", image: "/assets/images/top-picks/img1.jpg", description: "Sun, sand, and sea.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Popular Beach Area", category: "Nature", route: "TOP_PICKS" as RouteKey, place_id: "tp1_fallback", photo_urls: ["/assets/images/top-picks/img1.jpg"]}, - { id: "tp2", name: "Mountain Hike", image: "/assets/images/top-picks/img2.jpg", description: "Breathtaking views await.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Scenic Mountain Trails", category: "Adventure", route: "TOP_PICKS" as RouteKey, place_id: "tp2_fallback", photo_urls: ["/assets/images/top-picks/img2.jpg"]}, - { id: "tp3", name: "City Exploration", image: "/assets/images/top-picks/img3.jpg", description: "Discover urban wonders.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Downtown City Center", category: "Urban", route: "TOP_PICKS" as RouteKey, place_id: "tp3_fallback", photo_urls: ["/assets/images/top-picks/img3.jpg"]}, + { + id: "tp1", + name: "Beach Paradise", + image: "/assets/images/top-picks/img1.jpg", + description: "Sun, sand, and sea.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Popular Beach Area", + category: "Nature", + route: "TOP_PICKS" as RouteKey, + place_id: "tp1_fallback", + photo_urls: ["/assets/images/top-picks/img1.jpg"], + }, + { + id: "tp2", + name: "Mountain Hike", + image: "/assets/images/top-picks/img2.jpg", + description: "Breathtaking views await.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Scenic Mountain Trails", + category: "Adventure", + route: "TOP_PICKS" as RouteKey, + place_id: "tp2_fallback", + photo_urls: ["/assets/images/top-picks/img2.jpg"], + }, + { + id: "tp3", + name: "City Exploration", + image: "/assets/images/top-picks/img3.jpg", + description: "Discover urban wonders.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Downtown City Center", + category: "Urban", + route: "TOP_PICKS" as RouteKey, + place_id: "tp3_fallback", + photo_urls: ["/assets/images/top-picks/img3.jpg"], + }, ]; const entertainmentFallbackImages = [ - { id: "e1", name: "Live Music Venue", image: "/assets/images/entertainment/img1.jpg", description: "Local bands and artists.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Entertainment District", category: "Music", route: "ENTERTAINMENT" as RouteKey, place_id: "e1_fallback", photo_urls: ["/assets/images/entertainment/img1.jpg"]}, - { id: "e2", name: "Cinema Complex", image: "/assets/images/entertainment/img2.jpg", description: "Latest movie releases.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Mall Area", category: "Movies", route: "ENTERTAINMENT" as RouteKey, place_id: "e2_fallback", photo_urls: ["/assets/images/entertainment/img2.jpg"]}, + { + id: "e1", + name: "Live Music Venue", + image: "/assets/images/entertainment/img1.jpg", + description: "Local bands and artists.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Entertainment District", + category: "Music", + route: "ENTERTAINMENT" as RouteKey, + place_id: "e1_fallback", + photo_urls: ["/assets/images/entertainment/img1.jpg"], + }, + { + id: "e2", + name: "Cinema Complex", + image: "/assets/images/entertainment/img2.jpg", + description: "Latest movie releases.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Mall Area", + category: "Movies", + route: "ENTERTAINMENT" as RouteKey, + place_id: "e2_fallback", + photo_urls: ["/assets/images/entertainment/img2.jpg"], + }, ]; const cultureFallbackImages = [ - { id: "c1", name: "Historical Museum", image: "/assets/images/culture/img1.jpg", description: "Artifacts and exhibits.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Old Town", category: "History", route: "CULTURE" as RouteKey, place_id: "c1_fallback", photo_urls: ["/assets/images/culture/img1.jpg"]}, - { id: "c2", name: "Art Installation", image: "/assets/images/culture/img2.jpg", description: "Contemporary art pieces.", geometry: { location: { lat: 0, lng: 0 } }, vicinity: "Art District", category: "Art", route: "CULTURE" as RouteKey, place_id: "c2_fallback", photo_urls: ["/assets/images/culture/img2.jpg"]}, + { + id: "c1", + name: "Historical Museum", + image: "/assets/images/culture/img1.jpg", + description: "Artifacts and exhibits.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Old Town", + category: "History", + route: "CULTURE" as RouteKey, + place_id: "c1_fallback", + photo_urls: ["/assets/images/culture/img1.jpg"], + }, + { + id: "c2", + name: "Art Installation", + image: "/assets/images/culture/img2.jpg", + description: "Contemporary art pieces.", + geometry: { location: { lat: 0, lng: 0 } }, + vicinity: "Art District", + category: "Art", + route: "CULTURE" as RouteKey, + place_id: "c2_fallback", + photo_urls: ["/assets/images/culture/img2.jpg"], + }, ]; interface SectionDataState { @@ -60,59 +141,114 @@ export default function HomeScreen() { const [isGeocoding, setIsGeocoding] = useState(false); const homeSectionsConfig = [ - { id: "topPicks", titleKey: "topPicksTitle", queryTypes: ["tourist_attraction", "park"], radius: 5000, fallbackImageSet: topPicksFallbackImages, scrollButtonKey: "TOP_PICKS" as RouteKey }, - { id: "entertainment", titleKey: "entertainmentTitle", queryTypes: ["movie_theater", "night_club"], radius: 5000, fallbackImageSet: entertainmentFallbackImages, scrollButtonKey: "ENTERTAINMENT" as RouteKey }, - { id: "culture", titleKey: "cultureTitle", queryTypes: ["museum", "art_gallery"], radius: 5000, fallbackImageSet: cultureFallbackImages, scrollButtonKey: "CULTURE" as RouteKey }, + { + id: "topPicks", + titleKey: "topPicksTitle", + queryTypes: ["tourist_attraction", "park"], + radius: 5000, + fallbackImageSet: topPicksFallbackImages, + scrollButtonKey: "TOP_PICKS" as RouteKey, + }, + { + id: "entertainment", + titleKey: "entertainmentTitle", + queryTypes: ["movie_theater", "night_club"], + radius: 5000, + fallbackImageSet: entertainmentFallbackImages, + scrollButtonKey: "ENTERTAINMENT" as RouteKey, + }, + { + id: "culture", + titleKey: "cultureTitle", + queryTypes: ["museum", "art_gallery"], + radius: 5000, + fallbackImageSet: cultureFallbackImages, + scrollButtonKey: "CULTURE" as RouteKey, + }, ]; const getCurrentLatLng = (): Promise<{ lat: number; lng: number }> => { return new Promise((resolve) => { if (navigator.geolocation) { navigator.geolocation.getCurrentPosition( - (position) => resolve({ lat: position.coords.latitude, lng: position.coords.longitude }), + (position) => + resolve({ + lat: position.coords.latitude, + lng: position.coords.longitude, + }), (error) => { console.error("Error getting location:", error); - setErrorMsg("Permission to access location was denied. Using default location."); - resolve({ lat: 40.7128, lng: -74.006 }); // Default: New York + setErrorMsg( + "Location permission denied. Showing nearby places based on default location.", + ); + resolve({ lat: 40.7128, lng: -74.006 }); // New York fallback }, ); } else { - setErrorMsg("Geolocation is not supported. Using default location."); - resolve({ lat: 40.7128, lng: -74.006 }); // Default: New York + setErrorMsg("Geolocation not supported. Using default location."); + resolve({ lat: 40.7128, lng: -74.006 }); } }); }; useEffect(() => { - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - (position) => setLocationDisplay(`Lat: ${position.coords.latitude.toFixed(2)}, Lon: ${position.coords.longitude.toFixed(2)}`), - (error) => { setErrorMsg("Permission to access location was denied."); console.error(error); }, - ); - } else { - setErrorMsg("Geolocation is not supported by this browser."); - } - - const fetchAllSectionsData = async () => { + const fetchLocationAndData = async () => { const { lat, lng } = await getCurrentLatLng(); + setLocationDisplay(`Lat: ${lat.toFixed(2)}, Lon: ${lng.toFixed(2)}`); + const locationString = `${lat},${lng}`; + homeSectionsConfig.forEach((section) => { startTransition(async () => { - setSectionsData((prevData) => ({ ...prevData, [section.id]: { data: [], isLoading: true, error: null } })); + setSectionsData((prevData) => ({ + ...prevData, + [section.id]: { data: [], isLoading: true, error: null }, + })); + try { - const result = await getNearbyPlacesAction(locationString, section.queryTypes, section.radius); + const result = await getNearbyPlacesAction( + locationString, + section.queryTypes, + section.radius, + ); + if (result.success && result.data) { - setSectionsData((prevData) => ({ ...prevData, [section.id]: { data: result.data!.slice(0, 10), isLoading: false, error: null } })); + setSectionsData((prevData) => ({ + ...prevData, + [section.id]: { + data: result.data!.slice(0, 10), + isLoading: false, + error: null, + }, + })); } else { - setSectionsData((prevData) => ({ ...prevData, [section.id]: { data: [], isLoading: false, error: result.error || `Failed to fetch ${t(section.titleKey)}.` } })); + setSectionsData((prevData) => ({ + ...prevData, + [section.id]: { + data: [], + isLoading: false, + error: + result.error || `Failed to fetch ${t(section.titleKey)}.`, + }, + })); } } catch (error) { - setSectionsData((prevData) => ({ ...prevData, [section.id]: { data: [], isLoading: false, error: (error as Error).message || `Exception fetching ${t(section.titleKey)}.` } })); + setSectionsData((prevData) => ({ + ...prevData, + [section.id]: { + data: [], + isLoading: false, + error: + (error as Error).message || + `Exception fetching ${t(section.titleKey)}.`, + }, + })); } }); }); }; - fetchAllSectionsData(); + + fetchLocationAndData(); }, [t]); const handleSearchSubmit = async (query: string) => { @@ -148,21 +284,63 @@ export default function HomeScreen() { const handleNavigation = (routeKey: RouteKey) => { const path = ROUTES[routeKey]; - if (path) router.push(path); else console.error("Error: Invalid routeKey or path not found:", routeKey); + if (path) router.push(path); + else console.error("Error: Invalid routeKey or path not found:", routeKey); }; - const handleExternalLink = (url: string) => window.open(url, "_blank", "noopener noreferrer"); + const handleExternalLink = (url: string) => + window.open(url, "_blank", "noopener noreferrer"); const iconSize = 28; const cardButtons = [ - { title: "Stay", icon: , color: "text-red-500", onPress: () => handleNavigation("STAY") }, - { title: "Food", icon: , color: "text-orange-500", onPress: () => handleNavigation("FOOD") }, - { title: "Culture", icon: , color: "text-yellow-500", onPress: () => handleNavigation("CULTURE") }, - { title: "Entertainment", icon: , color: "text-green-500", onPress: () => handleNavigation("ENTERTAINMENT") }, - { title: "Nearby", icon: , color: "text-teal-500", onPress: () => handleNavigation("NEARBY") }, - { title: "Top Picks", icon: , color: "text-blue-500", onPress: () => handleNavigation("TOP_PICKS") }, - { title: "Emergency", icon: , color: "text-purple-500", onPress: () => handleNavigation("EMERGENCY") }, - { title: "Planner", icon: , color: "text-pink-500", onPress: () => handleNavigation("PLANNER") }, + { + title: "Stay", + icon: , + color: "text-red-500", + onPress: () => handleNavigation("STAY"), + }, + { + title: "Food", + icon: , + color: "text-orange-500", + onPress: () => handleNavigation("FOOD"), + }, + { + title: "Culture", + icon: , + color: "text-yellow-500", + onPress: () => handleNavigation("CULTURE"), + }, + { + title: "Entertainment", + icon: , + color: "text-green-500", + onPress: () => handleNavigation("ENTERTAINMENT"), + }, + { + title: "Nearby", + icon: , + color: "text-teal-500", + onPress: () => handleNavigation("NEARBY"), + }, + { + title: "Top Picks", + icon: , + color: "text-blue-500", + onPress: () => handleNavigation("TOP_PICKS"), + }, + { + title: "Emergency", + icon: , + color: "text-purple-500", + onPress: () => handleNavigation("EMERGENCY"), + }, + { + title: "Planner", + icon: , + color: "text-pink-500", + onPress: () => handleNavigation("PLANNER"), + }, ]; return ( @@ -171,12 +349,18 @@ export default function HomeScreen() {
-

{t("greeting")}

+

+ {t("greeting")} +

- {locationDisplay ? `Current Location: ${locationDisplay}` : errorMsg || "Fetching location..."} + {locationDisplay + ? `Current Location: ${locationDisplay}` + : errorMsg || "Fetching location..."}

-
U
+
+ U +
setSearchQuery(e.target.value)} onKeyDown={(e) => { - if (e.key === 'Enter') { + if (e.key === "Enter") { e.preventDefault(); // Prevent default if part of a form handleSearchSubmit(searchQuery); } @@ -199,34 +383,53 @@ export default function HomeScreen() {
{/* Geocoding Results Section */} -
{/* Added padding to match main content area */} +
+ {" "} + {/* Added padding to match main content area */} {isGeocoding && ( -
+
-

Geocoding address...

+

+ Geocoding address... +

)} {geocodingError && ( -
+
{geocodingError}
)} {!isGeocoding && !geocodingError && geocodedResults.length > 0 && ( -
-

Geocoding Results:

{/* Updated classes */} +
+

+ Geocoding Results: +

{" "} + {/* Updated classes */}
    {geocodedResults.map((result) => ( -
  • -

    {result.formatted_address}

    -

    Coordinates: Lat: {result.geometry.location.lat.toFixed(5)}, Lng: {result.geometry.location.lng.toFixed(5)}

    +
  • +

    + {result.formatted_address} +

    +

    + Coordinates: Lat:{" "} + {result.geometry.location.lat.toFixed(5)}, Lng:{" "} + {result.geometry.location.lng.toFixed(5)} +

  • ))}
)} - {!isGeocoding && !geocodingError && geocodedResults.length === 0 && searchQuery && ( -
- {/* This message shows after a search yields no results and isn't an error, or if search was cleared. + {!isGeocoding && + !geocodingError && + geocodedResults.length === 0 && + searchQuery && ( +
+ {/* This message shows after a search yields no results and isn't an error, or if search was cleared. To be more specific, a "No results found" is handled by geocodingError state. This part might need refinement based on desired UX for "empty search after submit" vs "initial state". For now, let's assume geocodingError handles "No results found." @@ -234,12 +437,16 @@ export default function HomeScreen() { So, this specific condition might not be hit if "No results" is always an error message. Let's remove this or make it more specific to "type to search". */} -
- )} +
+ )}
-
{/* Adjusted padding top to 0 as results section above has padding */} -
{/* Added mt-6 */} +
+ {" "} + {/* Adjusted padding top to 0 as results section above has padding */} +
+ {" "} + {/* Added mt-6 */}
{cardButtons.map((button) => ( ))}
- {homeSectionsConfig.map((sectionConfig) => { - const sectionState = sectionsData[sectionConfig.id] || { data: [], isLoading: true, error: null }; - const cardDataForScroll = sectionState.isLoading || sectionState.error || sectionState.data.length === 0 - ? sectionConfig.fallbackImageSet - : sectionState.data; + const sectionState = sectionsData[sectionConfig.id] || { + data: [], + isLoading: true, + error: null, + }; + const cardDataForScroll = + sectionState.isLoading || + sectionState.error || + sectionState.data.length === 0 + ? sectionConfig.fallbackImageSet + : sectionState.data; const imagesForScroll = cardDataForScroll.map((place) => { - if (place.photo_urls && place.photo_urls.length > 0) return place.photo_urls[0]; + if (place.photo_urls && place.photo_urls.length > 0) + return place.photo_urls[0]; if ((place as any).image) return (place as any).image; return "/assets/images/default-placeholder.png"; }); const finalCardData = cardDataForScroll.map((item, idx) => ({ - ...item, - place_id: item.place_id || (item as any).id || `fallback-${sectionConfig.id}-${idx}`, - name: item.name || (item as any).title || "Unknown Place", + ...item, + place_id: + item.place_id || + (item as any).id || + `fallback-${sectionConfig.id}-${idx}`, + name: item.name || (item as any).title || "Unknown Place", })); return ( -
{/* Added wrapper div with mb-6 */} +
+ {" "} + {/* Added wrapper div with mb-6 */}
); })} - -
{/* my-6 is already here, good */} -

Plan Your Stay

+
+ {" "} + {/* my-6 is already here, good */} +

+ Plan Your Stay +

-

© {new Date().getFullYear()} GoTogether. All rights reserved.

+

+ © {new Date().getFullYear()} GoTogether. All rights reserved. +

diff --git a/src/components/home/horizontal-scroll-bar.tsx b/src/components/home/horizontal-scroll-bar.tsx index 67d5310..af92263 100644 --- a/src/components/home/horizontal-scroll-bar.tsx +++ b/src/components/home/horizontal-scroll-bar.tsx @@ -1,13 +1,13 @@ "use client"; import { LocationDetail } from "@/types/location-types"; -import React, { useRef, useState, useEffect, useCallback } from "react"; -import { ChevronLeft, ChevronRight } from "lucide-react"; -import PlaceCard from "./PlaceCard"; // Import the new PlaceCard component -import type { RouteKey } from "@/lib/routes"; // Import RouteKey +import React from "react"; +import PlaceCard from "./PlaceCard"; +import type { RouteKey } from "@/lib/routes"; +import { ChevronRight } from "lucide-react"; interface ScrollButton { - route: RouteKey; // Changed string to RouteKey + route: RouteKey; loading: boolean; } @@ -15,124 +15,56 @@ interface HorizontalScrollBarProps { title: string; cardData: LocationDetail[]; scrollButton: ScrollButton; - handleNavigation: (route: RouteKey) => void; // Changed string to RouteKey + handleNavigation: (route: RouteKey) => void; images: string[]; } -const HorizontalScrollBar = ( - props: HorizontalScrollBarProps, -): React.JSX.Element => { - const { title, cardData, scrollButton, handleNavigation, images } = props; - const scrollContainerRef = useRef(null); - const [canScrollLeft, setCanScrollLeft] = useState(false); - const [canScrollRight, setCanScrollRight] = useState(false); - - const updateScrollButtonState = useCallback(() => { - if (scrollContainerRef.current) { - const { scrollLeft, scrollWidth, clientWidth } = - scrollContainerRef.current; - setCanScrollLeft(scrollLeft > 5); - setCanScrollRight(scrollLeft < scrollWidth - clientWidth - 5); - } - }, []); - - useEffect(() => { - const scrollElement = scrollContainerRef.current; - if (!scrollElement) return; - - updateScrollButtonState(); - - scrollElement.addEventListener("scroll", updateScrollButtonState); - const resizeObserver = new ResizeObserver(updateScrollButtonState); - resizeObserver.observe(scrollElement); - const mutationObserver = new MutationObserver(updateScrollButtonState); - mutationObserver.observe(scrollElement, { childList: true, subtree: true }); - - return () => { - scrollElement.removeEventListener("scroll", updateScrollButtonState); - resizeObserver.disconnect(); - mutationObserver.disconnect(); - }; - }, [cardData, updateScrollButtonState]); - - const handleScrollLeft = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollBy({ left: -300, behavior: "smooth" }); - } - }; - - const handleScrollRight = () => { - if (scrollContainerRef.current) { - scrollContainerRef.current.scrollBy({ left: 300, behavior: "smooth" }); - } - }; +const HorizontalScrollBar = ({ + title, + cardData, + scrollButton, + handleNavigation, + images, +}: HorizontalScrollBarProps): React.JSX.Element => { + const maxCards = 3; + const displayedCards = cardData.slice(0, maxCards); return ( -
-
-

{/* Changed font-bold to font-semibold */} +
+
+

{title}

-
- + {scrollButton?.route && ( - {scrollButton && scrollButton.route && scrollButton.route.trim() !== "" && ( - - )} -
+ )}
{scrollButton.loading ? ( -

+

Loading...

+ ) : displayedCards.length === 0 ? ( +

+ No items to display currently. +

) : ( -
- {cardData.length === 0 && !scrollButton.loading && ( -

- No items to display currently. -

- )} - {cardData.map((item, index) => { - const imagePath = - images?.[index] ?? "/assets/images/default-placeholder.png"; - - return ( - - ); - })} +
+ {displayedCards.map((item, index) => ( + + ))}
)}
diff --git a/src/components/shared/ClientImage.tsx b/src/components/shared/ClientImage.tsx index b735860..1c64c6f 100644 --- a/src/components/shared/ClientImage.tsx +++ b/src/components/shared/ClientImage.tsx @@ -12,7 +12,7 @@ interface ClientImageProps { priority?: boolean; className?: string; objectFit?: "cover" | "contain" | "fill"; - sizes?: string; // Added sizes prop + sizes?: string; } export default function ClientImage({ @@ -24,13 +24,17 @@ export default function ClientImage({ priority = false, className, objectFit = "cover", - sizes, // Added sizes to destructuring + sizes, }: ClientImageProps) { const [imgSrc, setImgSrc] = useState(src); + const [hasError, setHasError] = useState(false); const handleError = () => { - console.error(`Failed to load image at ${src}`); - setImgSrc("/assets/images/default-placeholder.png"); + if (!hasError) { + console.warn(`Image failed to load: ${imgSrc}`); + setImgSrc("/assets/images/default-placeholder.png"); + setHasError(true); + } }; return ( @@ -44,7 +48,7 @@ export default function ClientImage({ onError={handleError} className={className} style={{ objectFit }} - sizes={sizes} // Passed sizes to Image component + sizes={sizes} /> ); }