PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.16
Yatra – Travel Booking & Tour Operator Software v3.0.16
3.0.16 3.0.15 3.0.14 3.0.14.1 3.0.14.2 3.0.12 3.0.13 3.0.11 3.0.10 3.0.9 3.0.8 3.0.7 3.0.6 3.0.5 3.0.5.1 3.0.4 3.0.3 3.0.2.9 3.0.2.7 3.0.2.8 3.0.2.6 trunk 1.0.0 2.0.0 2.0.1 All 84 releases
yatra / resources / js / components / classifications / ClassificationLandingPageField.tsx

ClassificationLandingPageField.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.16, at resources/js/components/classifications/ClassificationLandingPageField.tsx

313 lines 8.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Landing page picker for classifications (metadata.landing_page_id).
3 * Shown only when Custom Landing Pages module is enabled. Public URL logic is in Yatra Pro.
4 */
5
6 import React, { useEffect, useMemo, useState } from "react";
7 import { __, sprintf } from "../../lib/i18n";
8 import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
9 import { Select } from "../ui/select";
10 import { Loader2 } from "lucide-react";
11
12 type WpPageSearchItem = {
13 id: number;
14 title?: { rendered?: string };
15 };
16
17 function restRoot(): string {
18 const raw = window.yatraAdmin?.restUrl || "/wp-json/";
19 return raw.endsWith("/") ? raw : `${raw}/`;
20 }
21
22 function parsePageTitle(payload: unknown): string {
23 if (!payload || typeof payload !== "object") {
24 return "";
25 }
26 const p = payload as WpPageSearchItem;
27 if (typeof p.title?.rendered === "string") {
28 return p.title.rendered.replace(/<[^>]+>/g, "").trim();
29 }
30 return "";
31 }
32
33 export function parseLandingPageIdFromMetadata(
34 metadata?: { landing_page_id?: unknown } | null,
35 ): number | null {
36 const raw = metadata?.landing_page_id;
37 if (raw == null || raw === "") {
38 return null;
39 }
40 const n = typeof raw === "number" ? raw : parseInt(String(raw).trim(), 10);
41 return Number.isFinite(n) && n > 0 ? n : null;
42 }
43
44 export async function fetchPublishedPagePermalink(
45 pageId: number,
46 ): Promise<string | null> {
47 if (!pageId) {
48 return null;
49 }
50 const nonce = window.yatraAdmin?.nonce || "";
51 const url = `${restRoot()}wp/v2/pages/${pageId}?context=embed`;
52 const res = await fetch(url, {
53 credentials: "same-origin",
54 headers: {
55 "X-WP-Nonce": nonce,
56 Accept: "application/json",
57 },
58 });
59 if (!res.ok) {
60 return null;
61 }
62 const data = await res.json();
63 if (typeof data?.link !== "string" || data.link === "") {
64 return null;
65 }
66
67 return data.link;
68 }
69
70 async function fetchPublishedPagesFirstPage(): Promise<{
71 items: WpPageSearchItem[];
72 total: number | null;
73 }> {
74 const nonce = window.yatraAdmin?.nonce || "";
75 const params = new URLSearchParams({
76 per_page: "100",
77 orderby: "title",
78 order: "asc",
79 status: "publish",
80 context: "embed",
81 });
82 const res = await fetch(`${restRoot()}wp/v2/pages?${params}`, {
83 credentials: "same-origin",
84 headers: {
85 "X-WP-Nonce": nonce,
86 Accept: "application/json",
87 },
88 });
89 if (!res.ok) {
90 return { items: [], total: null };
91 }
92 const data = await res.json();
93 const items = Array.isArray(data) ? data : [];
94 const totalRaw = res.headers.get("X-WP-Total");
95 const total =
96 totalRaw !== null && totalRaw !== "" ? parseInt(totalRaw, 10) : null;
97
98 return { items, total: Number.isFinite(total) ? total : null };
99 }
100
101 async function fetchSinglePublishedPage(
102 pageId: number,
103 ): Promise<WpPageSearchItem | null> {
104 const nonce = window.yatraAdmin?.nonce || "";
105 const res = await fetch(`${restRoot()}wp/v2/pages/${pageId}?context=embed`, {
106 credentials: "same-origin",
107 headers: {
108 "X-WP-Nonce": nonce,
109 Accept: "application/json",
110 },
111 });
112 if (!res.ok) {
113 return null;
114 }
115 const data = await res.json();
116 if (!data || typeof data.id !== "number") {
117 return null;
118 }
119
120 return data as WpPageSearchItem;
121 }
122
123 function sortPagesByTitle(list: WpPageSearchItem[]): WpPageSearchItem[] {
124 return [...list].sort((a, b) => {
125 const ta = parsePageTitle(a).toLowerCase();
126 const tb = parsePageTitle(b).toLowerCase();
127 return ta.localeCompare(tb);
128 });
129 }
130
131 function mergePageUnique(
132 list: WpPageSearchItem[],
133 extra: WpPageSearchItem,
134 ): WpPageSearchItem[] {
135 const id = Number(extra.id);
136 if (!Number.isFinite(id) || id <= 0) {
137 return list;
138 }
139 if (list.some((p) => Number(p.id) === id)) {
140 return list;
141 }
142
143 return sortPagesByTitle([...list, extra]);
144 }
145
146 interface ClassificationLandingPageFieldProps {
147 value: number | null;
148 onChange: (id: number | null) => void;
149 /** Unique HTML id prefix for the select (per form). */
150 selectId?: string;
151 }
152
153 export function ClassificationLandingPageField({
154 value,
155 onChange,
156 selectId = "yatra-classification-landing-page",
157 }: ClassificationLandingPageFieldProps) {
158 const moduleOn = Boolean(
159 typeof window !== "undefined" &&
160 window.yatraAdmin?.customLandingPagesModuleEnabled,
161 );
162
163 const [pages, setPages] = useState<WpPageSearchItem[]>([]);
164 const [loading, setLoading] = useState(true);
165 const [loadError, setLoadError] = useState(false);
166 const [totalPublished, setTotalPublished] = useState<number | null>(null);
167
168 useEffect(() => {
169 if (!moduleOn) {
170 return;
171 }
172
173 let cancelled = false;
174 void (async () => {
175 setLoading(true);
176 setLoadError(false);
177 try {
178 const { items, total } = await fetchPublishedPagesFirstPage();
179 if (cancelled) {
180 return;
181 }
182 setPages(sortPagesByTitle(items));
183 setTotalPublished(total);
184 } catch {
185 if (!cancelled) {
186 setLoadError(true);
187 setPages([]);
188 }
189 } finally {
190 if (!cancelled) {
191 setLoading(false);
192 }
193 }
194 })();
195
196 return () => {
197 cancelled = true;
198 };
199 }, [moduleOn]);
200
201 useEffect(() => {
202 if (!moduleOn || !value) {
203 return;
204 }
205 if (pages.some((p) => Number(p.id) === value)) {
206 return;
207 }
208
209 let cancelled = false;
210 void (async () => {
211 const one = await fetchSinglePublishedPage(value);
212 if (cancelled || !one) {
213 return;
214 }
215 setPages((prev) => mergePageUnique(prev, one));
216 })();
217
218 return () => {
219 cancelled = true;
220 };
221 }, [moduleOn, value, pages]);
222
223 const selectValue = useMemo(
224 () => (value && value > 0 ? String(value) : ""),
225 [value],
226 );
227
228 if (!moduleOn) {
229 return null;
230 }
231
232 return (
233 <Card>
234 <CardHeader className="pb-2">
235 <CardTitle className="text-base">
236 {__("Custom landing page", "yatra")}
237 </CardTitle>
238 </CardHeader>
239 <CardContent className="space-y-3 text-sm">
240 <p className="text-xs text-gray-600 dark:text-gray-400">
241 {__(
242 "Public catalog links and redirects use this WordPress page URL instead of the default Yatra taxonomy path.",
243 "yatra",
244 )}
245 </p>
246
247 <div className="space-y-1">
248 <label
249 htmlFor={selectId}
250 className="block text-sm font-medium text-gray-700 dark:text-gray-300"
251 >
252 {__("WordPress page", "yatra")}
253 </label>
254 {loading ? (
255 <div className="flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400 py-2">
256 <Loader2 className="w-4 h-4 animate-spin" />
257 {__("Loading pages…", "yatra")}
258 </div>
259 ) : loadError ? (
260 <p className="text-sm text-red-600 dark:text-red-400">
261 {__(
262 "Could not load pages. Check that you are logged in and try again.",
263 "yatra",
264 )}
265 </p>
266 ) : (
267 <Select
268 id={selectId}
269 className="w-full h-10"
270 value={selectValue}
271 onChange={(e) => {
272 const v = e.target.value;
273 if (v === "") {
274 onChange(null);
275 return;
276 }
277 const n = parseInt(v, 10);
278 onChange(Number.isFinite(n) && n > 0 ? n : null);
279 }}
280 >
281 <option value="">
282 {__("— Default Yatra URL (no custom page) —", "yatra")}
283 </option>
284 {pages.map((p) => {
285 const id = Number(p.id);
286 const label = parsePageTitle(p) || __("(no title)", "yatra");
287
288 return (
289 <option key={id} value={String(id)}>
290 {label}
291 </option>
292 );
293 })}
294 </Select>
295 )}
296 {totalPublished !== null && totalPublished > 100 && !loading && (
297 <p className="text-xs text-gray-500 dark:text-gray-400">
298 {sprintf(
299 // translators: %d: total number of published pages on the site.
300 __(
301 "Showing the first 100 published pages (of %d). Change the selection in WordPress → Pages if yours is not listed.",
302 "yatra",
303 ),
304 totalPublished,
305 )}
306 </p>
307 )}
308 </div>
309 </CardContent>
310 </Card>
311 );
312 }
313