227 lines
7.4 KiB
TypeScript
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>
|
|
);
|
|
}
|