From a02cf342a29149b3441bb255f5755dfa9e27797d Mon Sep 17 00:00:00 2001 From: HotelSync Date: Tue, 7 Apr 2026 21:57:46 +0300 Subject: [PATCH] =?UTF-8?q?feat:=20widget=20iframe=20embed=20=E2=80=94=20s?= =?UTF-8?q?tandalone=20/:slug/book=20page?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/App.tsx | 2 + src/pages/BookingWidgetPage.tsx | 31 ++++++----- src/pages/BookingWidgetStandalonePage.tsx | 66 +++++++++++++++++++++++ 3 files changed, 85 insertions(+), 14 deletions(-) create mode 100644 src/pages/BookingWidgetStandalonePage.tsx diff --git a/src/App.tsx b/src/App.tsx index a842662..36cf4cf 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,6 +51,7 @@ import { PaymentSettingsPage } from './pages/PaymentSettingsPage' import { DepositSettingsPage } from './pages/DepositSettingsPage' import { DepositHistoryPage } from './pages/DepositHistoryPage' import { PayDepositPage } from './pages/PayDepositPage' +import { BookingWidgetStandalonePage } from './pages/BookingWidgetStandalonePage' import { ModuleGuard } from './components/ModuleGuard' export default function App() { @@ -69,6 +70,7 @@ export default function App() { } /> } /> } /> + } /> {/* PMS routes */} }> diff --git a/src/pages/BookingWidgetPage.tsx b/src/pages/BookingWidgetPage.tsx index 64e1a6e..fe1d80d 100644 --- a/src/pages/BookingWidgetPage.tsx +++ b/src/pages/BookingWidgetPage.tsx @@ -14,7 +14,7 @@ import { api, type WidgetRoom, type WidgetCategory, type PaymentGateway } from ' // ── Widget settings type ─────────────────────────────────────────────────────── -interface FormField { +export interface FormField { id: string label: string type: 'text' | 'email' | 'phone' | 'textarea' | 'select' @@ -23,7 +23,7 @@ interface FormField { isCustom?: boolean } -interface AdditionalService { +export interface AdditionalService { id: string name: string icon: string @@ -32,7 +32,7 @@ interface AdditionalService { enabled: boolean } -interface WidgetSettings { +export interface WidgetSettings { hotelName: string primaryColor: string language: 'ru' | 'en' @@ -170,7 +170,7 @@ const AMENITY_ICONS: Record = { const EXTRA_BED_PRICE = 1500 -const DEFAULT_FORM_FIELDS: FormField[] = [ +export const DEFAULT_FORM_FIELDS: FormField[] = [ { id: 'name', label: 'Имя и фамилия', type: 'text', required: true, enabled: true }, { id: 'email', label: 'Email', type: 'email', required: true, enabled: true }, { id: 'phone', label: 'Телефон', type: 'phone', required: true, enabled: true }, @@ -178,7 +178,7 @@ const DEFAULT_FORM_FIELDS: FormField[] = [ { id: 'arrival', label: 'Время заезда', type: 'select', required: false, enabled: false }, ] -const DEFAULT_SERVICES: AdditionalService[] = [ +export const DEFAULT_SERVICES: AdditionalService[] = [ { id: 's1', name: 'Теннисный корт', icon: '🎾', price: 1200, unit: 'ч', enabled: true }, { id: 's2', name: 'Сауна', icon: '🧖', price: 2500, unit: 'ч', enabled: true }, { id: 's3', name: 'Банкетный зал', icon: '🎪', price: 15000, unit: 'день',enabled: false }, @@ -187,7 +187,7 @@ const DEFAULT_SERVICES: AdditionalService[] = [ // ── Widget Preview Component ─────────────────────────────────────────────────── -function WidgetPreview({ settings, slug, realRooms, realCategories, paymentEnabled }: { +export function WidgetPreview({ settings, slug, realRooms, realCategories, paymentEnabled }: { settings: WidgetSettings slug?: string realRooms?: WidgetRoom[] @@ -1170,14 +1170,17 @@ export function BookingWidgetPage() { formFields: prev.formFields.filter(f => f.id !== id), })) - const snippet = `` + const widgetUrl = `https://app.hotelsync.ru/${slug || 'grand-palace'}/book?color=${encodeURIComponent(settings.primaryColor)}&lang=${settings.language}&mode=${settings.roomDisplayMode}&min-nights=${settings.minNights}&rental=${settings.showRental}&promo=${settings.showPromo}&extra-beds=${settings.allowExtraBeds}&children=${settings.allowChildren}` + + const snippet = `` const copyCode = () => { navigator.clipboard.writeText(snippet).catch(() => {}) diff --git a/src/pages/BookingWidgetStandalonePage.tsx b/src/pages/BookingWidgetStandalonePage.tsx new file mode 100644 index 0000000..26fea2b --- /dev/null +++ b/src/pages/BookingWidgetStandalonePage.tsx @@ -0,0 +1,66 @@ +import { useState, useEffect } from 'react' +import { useParams, useSearchParams } from 'react-router-dom' +import { api, type WidgetRoom, type WidgetCategory } from '../lib/api' +import { WidgetPreview, DEFAULT_FORM_FIELDS, DEFAULT_SERVICES } from './BookingWidgetPage' +import type { WidgetSettings } from './BookingWidgetPage' + +export function BookingWidgetStandalonePage() { + const { slug } = useParams<{ slug: string }>() + const [searchParams] = useSearchParams() + + const color = searchParams.get('color') ?? '#4F46E5' + const lang = (searchParams.get('lang') ?? 'ru') as 'ru' | 'en' + const mode = (searchParams.get('mode') ?? 'rooms') as 'rooms' | 'categories' + const minNights = parseInt(searchParams.get('min-nights') ?? '1') || 1 + const showRental = searchParams.get('rental') === 'true' + const showPromo = searchParams.get('promo') !== 'false' + const extraBeds = searchParams.get('extra-beds') !== 'false' + const children = searchParams.get('children') !== 'false' + + const [realRooms, setRealRooms] = useState([]) + const [realCategories, setRealCategories] = useState([]) + const [hotelName, setHotelName] = useState('') + const [paymentEnabled, setPaymentEnabled] = useState(undefined) + + useEffect(() => { + if (!slug) return + api.widget.getConfig(slug) + .then(config => { + setRealRooms(config.rooms ?? []) + setRealCategories(config.categories ?? []) + setHotelName(config.hotelName ?? '') + setPaymentEnabled(config.paymentEnabled) + }) + .catch(() => {}) + }, [slug]) + + const settings: WidgetSettings = { + hotelName, + primaryColor: color, + language: lang, + showRooms: true, + showRental, + roomDisplayMode: mode, + minNights, + paymentProvider: 'yukassa', + showPromo, + allowExtraBeds: extraBeds, + allowChildren: children, + formFields: DEFAULT_FORM_FIELDS, + additionalServices: DEFAULT_SERVICES, + } + + return ( +
+
+ 0 ? realRooms : undefined} + realCategories={realCategories.length > 0 ? realCategories : undefined} + paymentEnabled={paymentEnabled} + /> +
+
+ ) +}