Email: Review-Link auf /review/:token umgestellt; Token-Erzeugung konsolidiert. Reviews: Client-Validation hinzugefügt. Verfügbarkeiten: Auto-Update nach Regelanlage. Galerie: Cover-Foto-Flag + Setzen im Admin, sofortige Aktualisierung nach Upload/Löschen/Reihenfolge-Änderung. Startseite: Featured-Foto = Reihenfolge 0, Seitenverhältnis beibehalten, Texte aktualisiert.
This commit is contained in:
226
src/client/components/profile-landing.tsx
Normal file
226
src/client/components/profile-landing.tsx
Normal file
@@ -0,0 +1,226 @@
|
||||
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>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user