Files
beauty-bookings/src/client/app.tsx

439 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { useState, useEffect } from "react";
import { useQuery } from "@tanstack/react-query";
import { queryClient } from "@/client/rpc-client";
import { useAuth } from "@/client/components/auth-provider";
import { LoginForm } from "@/client/components/login-form";
import { UserProfile } from "@/client/components/user-profile";
import { BookingForm } from "@/client/components/booking-form";
import { AdminTreatments } from "@/client/components/admin-treatments";
import { AdminBookings } from "@/client/components/admin-bookings";
import { AdminCalendar } from "@/client/components/admin-calendar";
import { InitialDataLoader } from "@/client/components/initial-data-loader";
import { AdminAvailability } from "@/client/components/admin-availability";
import { AdminGallery } from "@/client/components/admin-gallery";
import { AdminReviews } from "@/client/components/admin-reviews";
import BookingStatusPage from "@/client/components/booking-status-page";
import ReviewSubmissionPage from "@/client/components/review-submission-page";
import LegalPage from "@/client/components/legal-page";
import { ProfileLanding } from "@/client/components/profile-landing";
import { PWAInstallPrompt } from "@/client/components/pwa-install-prompt";
function App() {
const { user, isLoading, isOwner } = useAuth();
const [activeTab, setActiveTab] = useState<"profile-landing" | "booking" | "admin-treatments" | "admin-bookings" | "admin-calendar" | "admin-availability" | "admin-gallery" | "admin-reviews" | "profile" | "legal">("profile-landing");
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
const { data: socialMedia } = useQuery(
queryClient.social.getSocialMedia.queryOptions()
);
const hasSocialMedia = (socialMedia as any)?.tiktokProfile || (socialMedia as any)?.instagramProfile;
// Prevent background scroll when menu is open
useEffect(() => {
document.body.classList.toggle('overflow-hidden', isMobileMenuOpen);
return () => document.body.classList.remove('overflow-hidden');
}, [isMobileMenuOpen]);
// Handle booking status page
const path = window.location.pathname;
const PwaPrompt = <PWAInstallPrompt />;
if (path.startsWith('/booking/')) {
const token = path.split('/booking/')[1];
if (token) {
return <>
{PwaPrompt}
<BookingStatusPage token={token} />
</>;
}
}
// Handle review submission page
if (path.startsWith('/review/')) {
const token = path.split('/review/')[1];
if (token) {
return <>
{PwaPrompt}
<ReviewSubmissionPage token={token} />
</>;
}
}
// Show loading spinner while checking authentication
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-pink-50 to-purple-50">
<div className="text-center">
<img
src="/assets/stargilnails_logo_transparent_112.png"
alt="Stargil Nails Logo"
className="w-16 h-16 mx-auto mb-4 object-contain animate-pulse"
/>
<div className="text-lg text-gray-600">Lade...</div>
</div>
</div>
);
}
// Show login form if user is not authenticated and trying to access admin features
const needsAuth = !user && (activeTab === "admin-treatments" || activeTab === "admin-bookings" || activeTab === "admin-calendar" || activeTab === "admin-availability" || activeTab === "admin-gallery" || activeTab === "admin-reviews" || activeTab === "profile");
if (needsAuth) {
return <LoginForm />;
}
// Show legal page if legal tab is active
if (activeTab === "legal") {
return <LegalPage />;
}
const tabs = [
{ id: "profile-landing", label: "Startseite", icon: "🏠", requiresAuth: false },
{ id: "booking", label: "Termin buchen", icon: "📅", requiresAuth: false },
{ id: "legal", label: "Impressum/Datenschutz", icon: "📋", requiresAuth: false },
{ id: "admin-treatments", label: "Behandlungen verwalten", icon: "💅", requiresAuth: true },
{ id: "admin-bookings", label: "Buchungen verwalten", icon: "📋", requiresAuth: true },
{ id: "admin-calendar", label: "Kalender", icon: "📆", requiresAuth: true },
{ id: "admin-availability", label: "Verfügbarkeiten", icon: "⏰", requiresAuth: true },
{ id: "admin-gallery", label: "Photo-Wall", icon: "📸", requiresAuth: true },
{ id: "admin-reviews", label: "Bewertungen", icon: "⭐", requiresAuth: true },
...(user ? [{ id: "profile", label: "Profil", icon: "👤", requiresAuth: true }] : []),
] as const;
return (
<div className="min-h-screen bg-gradient-to-br from-pink-50 to-purple-50">
<InitialDataLoader />
{/* Header */}
<header className="bg-white shadow-sm border-b border-pink-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between items-center py-6">
<div
className="flex items-center space-x-3 cursor-pointer hover:opacity-80 transition-opacity"
onClick={() => setActiveTab("profile-landing")}
>
<img
src="/assets/stargilnails_logo_transparent_112.png"
alt="Stargil Nails Logo"
className="w-12 h-12 object-contain"
/>
<div>
<h1 className="text-2xl font-bold text-gray-900">Stargirlnails Kiel</h1>
<p className="text-sm text-gray-600">Professional Nail Design & Care</p>
</div>
</div>
{/* Hamburger Button für Mobile */}
<button
type="button"
aria-label="Menü öffnen"
aria-controls="mobile-menu"
aria-expanded={isMobileMenuOpen}
className="md:hidden p-2 -ml-2 text-3xl text-gray-700 hover:text-pink-600 transition-colors"
onClick={() => setIsMobileMenuOpen(true)}
>
</button>
{user && (
<div className="flex items-center space-x-4">
<span className="text-sm text-gray-600 hidden sm:inline">
Willkommen, {user.username}
</span>
<span className="text-sm text-gray-600 sm:hidden">
{user.username}
</span>
{isOwner && (
<span className="bg-pink-100 text-pink-800 px-2 py-1 rounded-full text-xs font-medium">
Inhaber
</span>
)}
</div>
)}
</div>
</div>
</header>
{/* Desktop Navigation */}
<nav className="bg-white shadow-sm hidden md:block">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex space-x-8">
{tabs.map((tab) => {
// Hide admin tabs for non-owners
if (tab.requiresAuth && !isOwner && tab.id !== "profile") {
return null;
}
return (
<button
key={tab.id}
onClick={() => {
if (tab.requiresAuth && !user) {
// This will trigger the login form
setActiveTab(tab.id as any);
} else {
setActiveTab(tab.id as any);
}
}}
className={`py-4 px-1 border-b-2 font-medium text-sm flex items-center space-x-2 ${
activeTab === tab.id
? "border-pink-500 text-pink-600"
: "border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300"
}`}
>
<span>{tab.icon}</span>
<span>{tab.label}</span>
</button>
);
})}
{!user && (
<button
onClick={() => setActiveTab("profile")} // This will trigger login
className="py-4 px-1 border-b-2 border-transparent text-gray-500 hover:text-gray-700 hover:border-gray-300 font-medium text-sm flex items-center space-x-2"
>
<span>🔑</span>
<span>Inhaber Login</span>
</button>
)}
</div>
</div>
</nav>
{/* Mobile Backdrop */}
{isMobileMenuOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 md:hidden"
onClick={() => setIsMobileMenuOpen(false)}
/>
)}
{/* Mobile Slide-in Panel */}
<div
id="mobile-menu"
role="dialog"
aria-modal="true"
aria-label="Navigation"
className={`fixed inset-y-0 left-0 w-64 bg-white shadow-xl z-50 md:hidden transform transition-transform duration-300 ease-in-out ${isMobileMenuOpen ? 'translate-x-0' : '-translate-x-full'}`}
>
<button
type="button"
aria-label="Menü schließen"
className="absolute top-4 right-4 text-2xl text-gray-600 hover:text-pink-600"
onClick={() => setIsMobileMenuOpen(false)}
>
×
</button>
<div className="p-6">
<img
src="/assets/stargilnails_logo_transparent_112.png"
alt="Stargil Nails Logo"
className="w-10 h-10 mb-6 object-contain"
/>
<nav className="mt-2 flex flex-col space-y-2">
{tabs.map((tab) => {
// Hide admin tabs for non-owners
if (tab.requiresAuth && !isOwner && tab.id !== 'profile') return null;
return (
<button
key={tab.id}
onClick={() => {
setActiveTab(tab.id as any);
setIsMobileMenuOpen(false);
}}
className={`flex items-center space-x-3 px-4 py-3 rounded-lg transition-colors ${
activeTab === tab.id ? 'bg-pink-100 text-pink-600' : 'text-gray-700 hover:bg-gray-100'
}`}
>
<span>{tab.icon}</span>
<span>{tab.label}</span>
</button>
);
})}
{!user && (
<button
onClick={() => {
setActiveTab('profile');
setIsMobileMenuOpen(false);
}}
className="flex items-center space-x-3 px-4 py-3 rounded-lg text-gray-700 hover:bg-gray-100 transition-colors"
>
<span>🔑</span>
<span>Inhaber Login</span>
</button>
)}
</nav>
</div>
</div>
{/* Main Content */}
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{activeTab === "profile-landing" && (
<ProfileLanding onNavigateToBooking={() => setActiveTab("booking")} />
)}
{activeTab === "booking" && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Buche deine perfekte Nagelbehandlung
</h2>
<p className="text-lg text-gray-600 max-w-2xl mx-auto">
Erlebe professionelle Nagelpflege mit unseren Experten.
Wähle aus unserem breiten Angebot an Behandlungen und buche noch heute deinen Termin.
</p>
</div>
<BookingForm />
</div>
)}
{activeTab === "admin-treatments" && isOwner && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Behandlungen verwalten
</h2>
<p className="text-lg text-gray-600">
Füge Behandlungen hinzu, bearbeite und verwalte deine Nagelbehandlungen.
</p>
</div>
<AdminTreatments />
</div>
)}
{activeTab === "admin-bookings" && isOwner && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Buchungen verwalten
</h2>
<p className="text-lg text-gray-600">
Sieh dir Kundentermine an und verwalte Buchungen.
</p>
</div>
<AdminBookings />
</div>
)}
{activeTab === "admin-calendar" && isOwner && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Kalender - Bevorstehende Buchungen
</h2>
<p className="text-lg text-gray-600">
Übersichtliche Darstellung aller bevorstehenden Termine im Kalenderformat.
</p>
</div>
<AdminCalendar />
</div>
)}
{activeTab === "admin-availability" && isOwner && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Verfügbarkeiten verwalten
</h2>
<p className="text-lg text-gray-600">
Verwalte wiederkehrende Zeiten und Urlaubszeiten.
</p>
</div>
<AdminAvailability />
</div>
)}
{activeTab === "admin-gallery" && isOwner && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Photo-Wall verwalten
</h2>
<p className="text-lg text-gray-600">
Lade Fotos hoch und verwalte deine Galerie.
</p>
</div>
<AdminGallery />
</div>
)}
{activeTab === "admin-reviews" && isOwner && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Bewertungen verwalten
</h2>
<p className="text-lg text-gray-600">
Prüfe und verwalte Kundenbewertungen.
</p>
</div>
<AdminReviews />
</div>
)}
{activeTab === "profile" && user && (
<div>
<div className="text-center mb-8">
<h2 className="text-3xl font-bold text-gray-900 mb-4">
Benutzerprofil
</h2>
<p className="text-lg text-gray-600">
Verwalte deine Kontoinformationen und Einstellungen.
</p>
</div>
<UserProfile />
</div>
)}
</main>
{/* PWA Installation Prompt for iOS */}
<PWAInstallPrompt hidden={isMobileMenuOpen} />
{/* Footer */}
<footer className="bg-white border-t border-pink-100 mt-16">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div className="text-center text-gray-600">
<p className="mb-4">&copy; 2025 Stargirlnails Kiel. Professional nail design & care with 🩷 and passion in Kiel 🌇.</p>
{hasSocialMedia && (
<div className="flex justify-center items-center gap-3 mt-4">
{(socialMedia as any)?.instagramProfile && (
<a
href={(socialMedia as any).instagramProfile}
target="_blank"
rel="noopener noreferrer"
className="text-pink-600 hover:text-pink-700 transition-colors"
aria-label="Instagram"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.07-4.85.07-3.204 0-3.584-.012-4.849-.07-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/>
</svg>
</a>
)}
{(socialMedia as any)?.tiktokProfile && (
<a
href={(socialMedia as any).tiktokProfile}
target="_blank"
rel="noopener noreferrer"
className="text-gray-800 hover:text-gray-900 transition-colors"
aria-label="TikTok"
>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M19.59 6.69a4.83 4.83 0 0 1-3.77-4.25V2h-3.45v13.67a2.89 2.89 0 0 1-5.2 1.74 2.89 2.89 0 0 1 2.31-4.64 2.93 2.93 0 0 1 .88.13V9.4a6.84 6.84 0 0 0-1-.05A6.33 6.33 0 0 0 5 20.1a6.34 6.34 0 0 0 10.86-4.43v-7a8.16 8.16 0 0 0 4.77 1.52v-3.4a4.85 4.85 0 0 1-1-.1z"/>
</svg>
</a>
)}
</div>
)}
</div>
</div>
</footer>
</div>
);
}
export default App;