Files
beauty-bookings/src/client/components/profile-landing.tsx

227 lines
7.4 KiB
TypeScript

import { useQuery } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client";
interface ProfileLandingProps {
onNavigateToBooking: () => void;
}
function StarRating({ rating }: { rating: number }) {
return (
<div className="flex space-x-1">
{[1, 2, 3, 4, 5].map((star) => (
<span
key={star}
className={star <= rating ? "text-yellow-400" : "text-gray-300"}
>
</span>
))}
</div>
);
}
function getDayName(dayOfWeek: number): string {
const days = [
"Sonntag",
"Montag",
"Dienstag",
"Mittwoch",
"Donnerstag",
"Freitag",
"Samstag",
];
return days[dayOfWeek];
}
function formatDate(date: Date): string {
return date.toLocaleDateString("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
});
}
export function ProfileLanding({ onNavigateToBooking }: ProfileLandingProps) {
// Data fetching with live queries
const { data: galleryPhotos = [] } = useQuery(
queryClient.gallery.live.listPhotos.experimental_liveOptions()
);
const sortedPhotos = ([...galleryPhotos] as any[]).sort((a, b) => (a.order || 0) - (b.order || 0));
const featuredPhoto = sortedPhotos[0];
const { data: reviews = [] } = useQuery(
queryClient.reviews.live.listPublishedReviews.experimental_liveOptions()
);
const { data: recurringRules = [] } = useQuery(
queryClient.recurringAvailability.live.listRules.experimental_liveOptions()
);
// Calculate next 7 days for opening hours
const getNext7Days = () => {
const days: Date[] = [];
const today = new Date();
for (let i = 0; i < 7; i++) {
const date = new Date(today);
date.setDate(today.getDate() + i);
days.push(date);
}
return days;
};
const next7Days = getNext7Days();
return (
<div className="max-w-6xl mx-auto space-y-12 py-8">
{/* Hero Section */}
<div className="bg-gradient-to-r from-pink-500 to-purple-600 rounded-lg shadow-lg p-8 text-white">
<h1 className="text-4xl md:text-5xl font-bold mb-4">
Stargirlnails Kiel
</h1>
<p className="text-xl mb-2">Professionelles Nageldesign und -Pflege in Kiel</p>
<p className="text-lg mb-8 opacity-90">
Lass dich von mir verwöhnen und genieße hochwertige Nail Art und Pflegebehandlungen.
</p>
<button
onClick={onNavigateToBooking}
className="bg-pink-600 text-white py-4 px-8 rounded-lg hover:bg-pink-700 text-lg font-semibold shadow-lg transition-colors w-full md:w-auto"
>
Termin buchen
</button>
</div>
{/* Featured Section: Erstes Foto (Reihenfolge 0) */}
{featuredPhoto && (
<div className="bg-white rounded-lg shadow-lg p-0 overflow-hidden">
<img
src={(featuredPhoto as any).base64Data}
alt={(featuredPhoto as any).title || "Featured"}
className="w-full h-auto object-contain"
loading="eager"
decoding="async"
/>
{(featuredPhoto as any).title && (
<div className="p-4 border-t">
<h2 className="text-xl font-semibold text-gray-900">{(featuredPhoto as any).title}</h2>
</div>
)}
</div>
)}
{/* Photo Gallery Section */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">Unsere Arbeiten</h2>
{galleryPhotos.length > 0 ? (
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{(sortedPhotos as typeof galleryPhotos)
.filter((p) => (featuredPhoto ? (p as any).id !== (featuredPhoto as any).id : true))
.slice(0, 9)
.map((photo, index) => (
<img
key={photo.id || index}
src={photo.base64Data}
alt={photo.title || "Gallery"}
loading="lazy"
decoding="async"
className="w-full h-48 object-cover rounded-lg shadow-md"
/>
))}
</div>
) : (
<p className="text-gray-600 text-center py-8">
Galerie wird bald aktualisiert
</p>
)}
</div>
{/* Opening Hours Section */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Öffnungszeiten (Nächste 7 Tage)
</h2>
<div className="space-y-2">
{next7Days.map((date, index) => {
const dayOfWeek = date.getDay();
const dayRules = recurringRules.filter(
(rule) => rule.isActive && rule.dayOfWeek === dayOfWeek
);
const sorted = [...dayRules].sort((a, b) =>
a.startTime.localeCompare(b.startTime)
);
return (
<div
key={index}
className={`p-4 rounded-lg ${
index % 2 === 0 ? "bg-gray-50" : "bg-white"
}`}
>
<div className="flex justify-between items-center">
<span className="font-semibold text-gray-900">
{getDayName(dayOfWeek)}, {formatDate(date)}
</span>
<div className="text-gray-700">
{dayRules.length > 0 ? (
<div className="space-y-1">
{sorted.map((rule) => (
<div
key={`${rule.dayOfWeek}-${rule.startTime}-${rule.endTime}`}
>
{rule.startTime} - {rule.endTime} Uhr
</div>
))}
</div>
) : (
<span className="text-gray-500">Geschlossen</span>
)}
</div>
</div>
</div>
);
})}
</div>
</div>
{/* Customer Reviews Section */}
<div className="bg-white rounded-lg shadow-lg p-6">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Kundenbewertungen
</h2>
{reviews.length > 0 ? (
<div className="space-y-4">
{reviews.slice(0, 10).map((review) => (
<div
key={
(review as any).id ||
(review as any).bookingId ||
`${(review as any).createdAt}-${(review as any).customerName}`
}
className="bg-gray-50 p-4 rounded-lg shadow-md"
>
<div className="flex justify-between items-start mb-2">
<div>
<h3 className="font-semibold text-gray-900">
{review.customerName}
</h3>
<StarRating rating={review.rating} />
</div>
<span className="text-sm text-gray-500">
{new Date(review.createdAt).toLocaleDateString("de-DE")}
</span>
</div>
{review.comment && (
<p className="text-gray-700 mt-2">{review.comment}</p>
)}
</div>
))}
</div>
) : (
<p className="text-gray-600 text-center py-8">
Noch keine Bewertungen vorhanden
</p>
)}
</div>
</div>
);
}