PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.3
Yatra – Travel Booking & Tour Operator Software v3.0.3
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 / pages / CategoryForm.tsx

CategoryForm.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.3, at resources/js/pages/CategoryForm.tsx

810 lines 29.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 /**
2 * Category Form Page
3 * Add/Edit Trip Category with parent/subcategory support
4 */
5
6 import React, { useState, useEffect, useMemo } from "react";
7 import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
8 import { ArrowLeft, Save, Loader2, Edit2, X, Eye } from "lucide-react";
9 import { __ } from "../lib/i18n";
10 import { usePermissions } from "../hooks/usePermissions";
11 import { useToast } from "../components/ui/toast";
12 import { fetchSettings } from "../api/settings-api";
13 import { apiClient } from "../lib/api-client";
14 import { generateSlug } from "../lib/slug";
15 import { buildYatraSinglePreviewUrl } from "../lib/frontend-permalink-urls";
16 import { Button } from "../components/ui/button";
17 import { Input } from "../components/ui/input";
18 import { Select } from "../components/ui/select";
19 import { PageHeader } from "../components/common/PageHeader";
20 import {
21 Card,
22 CardContent,
23 CardHeader,
24 CardTitle,
25 } from "../components/ui/card";
26 import { ConditionalRender } from "../components/ui/conditional-render";
27 import { IconPicker, IconPickerValue } from "../components/ui/icon-picker";
28 import { RichTextEditor } from "../components/ui/rich-text-editor";
29
30 /** DB / REST may send 0|1 as number or string; never use Boolean() on string "0". */
31 function parseTripCategoryIsFeatured(raw: unknown): boolean {
32 if (raw === true || raw === 1) {
33 return true;
34 }
35 if (raw === false || raw === 0 || raw === null || raw === undefined) {
36 return false;
37 }
38 if (typeof raw === "string") {
39 const t = raw.trim();
40 return t === "1" || t.toLowerCase() === "true";
41 }
42 return Number(raw) === 1;
43 }
44
45 interface CategoryFormData {
46 name: string;
47 slug: string;
48 description: string;
49 icon: IconPickerValue | null;
50 parent_id: number | "";
51 status: string;
52 /** Trip category card "Featured" badge on shortcode / Trip Category block */
53 is_featured: boolean;
54 seo_title: string;
55 seo_description: string;
56 seo_keywords: string;
57 }
58
59 interface CategoryOption {
60 id: number;
61 name: string;
62 parent_id: number | null;
63 }
64
65 const CategoryForm: React.FC = () => {
66 const queryClient = useQueryClient();
67 const { can } = usePermissions();
68 const { showToast } = useToast();
69 const [formData, setFormData] = useState<CategoryFormData>({
70 name: "",
71 slug: "",
72 description: "",
73 icon: null,
74 parent_id: "",
75 status: "publish",
76 is_featured: false,
77 seo_title: "",
78 seo_description: "",
79 seo_keywords: "",
80 });
81 const [errors, setErrors] = useState<Record<string, string>>({});
82 const [isSubmitting, setIsSubmitting] = useState(false);
83 const [isSlugEditable, setIsSlugEditable] = useState(false);
84
85 const action = useMemo(() => {
86 const params = new URLSearchParams(window.location.search);
87 return params.get("action") || "create";
88 }, []);
89
90 const categoryId = useMemo(() => {
91 const params = new URLSearchParams(window.location.search);
92 return params.get("id") ? parseInt(params.get("id") || "0", 10) : null;
93 }, []);
94
95 const isEditMode = action === "edit" && categoryId !== null;
96
97 // Fetch category data if editing
98 const { data: categoryData, isLoading: isLoadingCategory } = useQuery({
99 queryKey: ["trip-category", categoryId],
100 queryFn: async () => {
101 if (!categoryId) return null;
102 try {
103 const response = await apiClient.get(`/trip-categories/${categoryId}`);
104 return response;
105 } catch (error: any) {
106 showToast(
107 error?.message || __("Failed to load category", "yatra"),
108 "error",
109 );
110 throw error;
111 }
112 },
113 enabled: isEditMode && can("yatra_view_trips"),
114 });
115
116 // Fetch parent category options (top-level categories)
117 const { data: parentCategoriesData } = useQuery({
118 queryKey: ["trip-categories", "parent-options"],
119 queryFn: async () => {
120 try {
121 const response = await apiClient.get("/trip-categories", {
122 params: {
123 per_page: 100,
124 parent_id: null,
125 hierarchical: false,
126 orderby: "name",
127 order: "ASC",
128 },
129 });
130 const payload = response?.data || response;
131 if (!payload) {
132 return [];
133 }
134 if (Array.isArray(payload)) {
135 return payload;
136 }
137 return payload.data || [];
138 } catch (error) {
139 return [];
140 }
141 },
142 enabled: can("yatra_view_trips"),
143 });
144
145 // Fetch settings for permalink handling
146 const { data: settings } = useQuery({
147 queryKey: ["settings"],
148 queryFn: async () => {
149 try {
150 const response = await fetchSettings();
151 return response?.data || response;
152 } catch (error) {
153 return {};
154 }
155 },
156 });
157
158 const parentOptions: CategoryOption[] = useMemo(() => {
159 if (!parentCategoriesData || !Array.isArray(parentCategoriesData)) {
160 return [];
161 }
162 return parentCategoriesData.filter((cat: any) => {
163 if (!cat || typeof cat !== "object") return false;
164 // Only allow top-level categories (no parent)
165 if (cat.parent_id) return false;
166 // Prevent selecting itself as parent when editing
167 if (isEditMode && categoryId && cat.id === categoryId) return false;
168 return true;
169 });
170 }, [parentCategoriesData, isEditMode, categoryId]);
171
172 // Load category data into form when editing
173 useEffect(() => {
174 if (categoryData && isEditMode) {
175 setFormData({
176 name: categoryData.name || "",
177 slug: categoryData.slug || "",
178 description: categoryData.description || "",
179 icon: (categoryData.icon as IconPickerValue) || null,
180 parent_id: categoryData.parent_id ?? "",
181 status: categoryData.status || "publish",
182 is_featured: parseTripCategoryIsFeatured(
183 (categoryData as { is_featured?: unknown }).is_featured,
184 ),
185 seo_title: categoryData.metadata?.seo_title || "",
186 seo_description: categoryData.metadata?.seo_description || "",
187 seo_keywords: categoryData.metadata?.seo_keywords || "",
188 });
189 }
190 }, [categoryData, isEditMode]);
191
192 const handleNameChange = (value: string) => {
193 // Only auto-generate slug on create; in edit mode, keep existing slug
194 if (!isEditMode && !isSlugEditable) {
195 const newSlug = generateSlug(value);
196 setFormData((prev) => ({
197 ...prev,
198 name: value,
199 slug: newSlug,
200 }));
201 } else {
202 setFormData((prev) => ({ ...prev, name: value }));
203 }
204 if (errors.name) {
205 setErrors((prev) => ({ ...prev, name: "" }));
206 }
207 };
208
209 const handleSlugChange = (value: string) => {
210 if (isSlugEditable) {
211 setFormData((prev) => ({ ...prev, slug: value }));
212 if (errors.slug) {
213 setErrors((prev) => ({ ...prev, slug: "" }));
214 }
215 }
216 };
217
218 const handleToggleSlugEdit = () => {
219 if (isSlugEditable) {
220 const newSlug = generateSlug(formData.name);
221 setFormData((prev) => ({ ...prev, slug: newSlug }));
222 }
223 setIsSlugEditable(!isSlugEditable);
224 };
225
226 const handleFieldChange = (field: keyof CategoryFormData, value: any) => {
227 setFormData((prev) => ({ ...prev, [field]: value }));
228 if (errors[field]) {
229 setErrors((prev) => ({ ...prev, [field]: "" }));
230 }
231 };
232
233 const validateForm = (): boolean => {
234 const newErrors: Record<string, string> = {};
235
236 if (!formData.name.trim()) {
237 newErrors.name = __("Name is required", "yatra");
238 }
239
240 if (!formData.slug.trim()) {
241 newErrors.slug = __("Slug is required", "yatra");
242 } else if (!/^[a-z0-9-]+$/.test(formData.slug)) {
243 newErrors.slug = __(
244 "Slug can only contain lowercase letters, numbers, and hyphens",
245 "yatra",
246 );
247 }
248
249 if (
250 formData.parent_id &&
251 categoryId &&
252 Number(formData.parent_id) === categoryId
253 ) {
254 newErrors.parent_id = __("Category cannot be its own parent", "yatra");
255 }
256
257 setErrors(newErrors);
258 return Object.keys(newErrors).length === 0;
259 };
260
261 const saveMutation = useMutation({
262 mutationFn: async (data: CategoryFormData) => {
263 const payload: any = {
264 name: data.name.trim(),
265 slug: data.slug.trim(),
266 description: data.description.trim(),
267 icon: data.icon,
268 parent_id: data.parent_id === "" ? null : Number(data.parent_id),
269 status: data.status,
270 is_featured: data.is_featured ? 1 : 0,
271 seo_title: data.seo_title.trim(),
272 seo_description: data.seo_description.trim(),
273 seo_keywords: data.seo_keywords.trim(),
274 };
275
276 if (isEditMode && isSlugEditable) {
277 payload.preserve_slug = true;
278 }
279
280 if (isEditMode && categoryId) {
281 return await apiClient.put(`/trip-categories/${categoryId}`, payload);
282 }
283 return await apiClient.post("/trip-categories", payload);
284 },
285 onSuccess: (response) => {
286 queryClient.invalidateQueries({ queryKey: ["trip-categories"] });
287 queryClient.invalidateQueries({
288 queryKey: ["trip-category", categoryId],
289 });
290 showToast(
291 isEditMode
292 ? __("Category updated successfully", "yatra")
293 : __("Category created successfully", "yatra"),
294 "success",
295 );
296 setIsSubmitting(false);
297
298 if (!isEditMode) {
299 const newId = response?.id;
300 if (newId) {
301 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories&action=edit&id=${newId}`;
302 } else {
303 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories`;
304 }
305 }
306 },
307 onError: (error: any) => {
308 const errorMessage =
309 error?.message ||
310 __("An error occurred while saving the category", "yatra");
311 showToast(errorMessage, "error");
312 setIsSubmitting(false);
313 },
314 });
315
316 const handleSubmit = (e: React.FormEvent) => {
317 e.preventDefault();
318
319 if (!validateForm()) {
320 showToast(__("Please fix the form errors", "yatra"), "warning");
321 return;
322 }
323
324 setIsSubmitting(true);
325 setErrors({});
326 saveMutation.mutate(formData);
327 };
328
329 const handleCancel = () => {
330 window.location.href = `${window.yatraAdmin?.siteUrl || ""}/wp-admin/admin.php?page=yatra&subpage=trips&tab=categories`;
331 };
332
333 if (isEditMode && isLoadingCategory) {
334 return (
335 <div className="space-y-3">
336 {/* Header Skeleton */}
337 <div className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg p-6">
338 <div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-2 animate-pulse"></div>
339 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-1/2 animate-pulse"></div>
340 </div>
341
342 {/* Form Skeleton */}
343 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
344 {/* Main Fields */}
345 <div className="lg:col-span-2 space-y-3">
346 <Card>
347 <CardContent className="p-6 space-y-4">
348 {/* Name field */}
349 <div>
350 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2 animate-pulse"></div>
351 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
352 </div>
353 {/* Slug field */}
354 <div>
355 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2 animate-pulse"></div>
356 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
357 </div>
358 {/* Description field */}
359 <div>
360 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2 animate-pulse"></div>
361 <div className="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
362 </div>
363 {/* Icon field */}
364 <div>
365 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-20 mb-2 animate-pulse"></div>
366 <div className="h-32 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
367 </div>
368 </CardContent>
369 </Card>
370 </div>
371
372 {/* Sidebar */}
373 <div className="space-y-3">
374 <Card>
375 <CardContent className="p-6 space-y-4">
376 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-24 mb-2 animate-pulse"></div>
377 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
378 </CardContent>
379 </Card>
380 <Card>
381 <CardContent className="p-6 space-y-4">
382 <div className="h-4 bg-gray-200 dark:bg-gray-700 rounded w-16 mb-2 animate-pulse"></div>
383 <div className="h-10 bg-gray-200 dark:bg-gray-700 rounded animate-pulse"></div>
384 </CardContent>
385 </Card>
386 </div>
387 </div>
388 </div>
389 );
390 }
391
392 return (
393 <div className="space-y-3">
394 <PageHeader
395 title={
396 isEditMode
397 ? __("Edit Category", "yatra")
398 : __("Add New Category", "yatra")
399 }
400 description={
401 isEditMode
402 ? __("Update category information", "yatra")
403 : __("Create a new trip category", "yatra")
404 }
405 actions={
406 <div className="flex items-center gap-2">
407 <Button
408 variant="outline"
409 onClick={handleCancel}
410 className="flex items-center gap-2"
411 >
412 <ArrowLeft className="w-4 h-4" />
413 {__("Back", "yatra")}
414 </Button>
415 {formData.slug && (
416 <Button
417 variant="outline"
418 onClick={() => {
419 const targetUrl = buildYatraSinglePreviewUrl({
420 entity: "category",
421 slug: formData.slug || "",
422 bases: settings as Record<string, unknown> | null,
423 });
424 window.open(targetUrl, "_blank", "noopener,noreferrer");
425 }}
426 className="flex items-center gap-2"
427 title={__("Preview category in new tab", "yatra")}
428 >
429 <Eye className="w-4 h-4" />
430 {__("Preview", "yatra")}
431 </Button>
432 )}
433 </div>
434 }
435 />
436
437 <ConditionalRender capability="yatra_edit_trips">
438 <form onSubmit={handleSubmit}>
439 <div className="grid grid-cols-1 lg:grid-cols-3 gap-3">
440 <div className="lg:col-span-2 space-y-3">
441 <Card>
442 <CardHeader className="pb-2">
443 <CardTitle className="text-base">
444 {__("Basic Information", "yatra")}
445 </CardTitle>
446 </CardHeader>
447 <CardContent className="space-y-3">
448 <div>
449 <label
450 htmlFor="name"
451 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
452 >
453 {__("Name", "yatra")}{" "}
454 <span className="text-red-500">*</span>
455 </label>
456 <Input
457 id="name"
458 type="text"
459 value={formData.name}
460 onChange={(e) => handleNameChange(e.target.value)}
461 placeholder={__("Enter category name", "yatra")}
462 className={errors.name ? "border-red-500" : ""}
463 required
464 />
465 {errors.name && (
466 <p className="mt-1 text-sm text-red-500">{errors.name}</p>
467 )}
468 </div>
469
470 <div>
471 <label
472 htmlFor="slug"
473 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
474 >
475 {__("Slug", "yatra")}{" "}
476 <span className="text-red-500">*</span>
477 </label>
478 <div className="relative">
479 <Input
480 id="slug"
481 type="text"
482 value={formData.slug}
483 onChange={(e) => handleSlugChange(e.target.value)}
484 placeholder={__("category-slug", "yatra")}
485 className={`pr-10 ${errors.slug ? "border-red-500" : ""} ${!isSlugEditable ? "bg-gray-50 dark:bg-gray-800 cursor-not-allowed" : ""}`}
486 disabled={!isSlugEditable}
487 required
488 />
489 <button
490 type="button"
491 onClick={handleToggleSlugEdit}
492 className="absolute right-2 top-1/2 -translate-y-1/2 p-1.5 text-gray-400 hover:text-gray-600 dark:hover:text-gray-300 transition-colors rounded"
493 aria-label={
494 isSlugEditable
495 ? __("Cancel editing slug", "yatra")
496 : __("Edit slug", "yatra")
497 }
498 >
499 {isSlugEditable ? (
500 <X className="w-4 h-4" />
501 ) : (
502 <Edit2 className="w-4 h-4" />
503 )}
504 </button>
505 </div>
506 {errors.slug && (
507 <p className="mt-1 text-sm text-red-500">{errors.slug}</p>
508 )}
509 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
510 {isSlugEditable
511 ? __(
512 "Manually editing slug. Click X to cancel and regenerate from name.",
513 "yatra",
514 )
515 : __(
516 "Auto-generated from name. Click edit icon to customize.",
517 "yatra",
518 )}
519 </p>
520 </div>
521
522 {/* Description */}
523 <div className="mt-4">
524 <RichTextEditor
525 label={__("Description", "yatra")}
526 value={formData.description || ""}
527 onChange={(value) =>
528 handleFieldChange("description", value)
529 }
530 placeholder={__("Enter category description...", "yatra")}
531 helperText={__(
532 "Describe what this category is for and what types of trips it contains.",
533 "yatra",
534 )}
535 minHeight={120}
536 maxHeight={360}
537 />
538 </div>
539 </CardContent>
540 </Card>
541 </div>
542
543 <div className="space-y-3">
544 <Card>
545 <CardHeader className="pb-2">
546 <CardTitle className="text-base">
547 {__("Hierarchy", "yatra")}
548 </CardTitle>
549 </CardHeader>
550 <CardContent className="space-y-3">
551 <div>
552 <label
553 htmlFor="parent_id"
554 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1.5"
555 >
556 {__("Parent Category", "yatra")}
557 </label>
558 <Select
559 id="parent_id"
560 value={
561 formData.parent_id === ""
562 ? ""
563 : String(formData.parent_id)
564 }
565 onChange={(e) =>
566 handleFieldChange(
567 "parent_id",
568 e.target.value === "" ? "" : Number(e.target.value),
569 )
570 }
571 className="w-full h-10"
572 >
573 <option value="">
574 {__("None (Top Level)", "yatra")}
575 </option>
576 {parentOptions.map((cat) => (
577 <option key={cat.id} value={cat.id}>
578 {cat.name}
579 </option>
580 ))}
581 </Select>
582 {errors.parent_id && (
583 <p className="mt-1 text-sm text-red-500">
584 {errors.parent_id}
585 </p>
586 )}
587 <p className="mt-1 text-xs text-gray-500 dark:text-gray-400">
588 {__(
589 "Assign a parent category to create subcategories.",
590 "yatra",
591 )}
592 </p>
593 </div>
594 </CardContent>
595 </Card>
596
597 <Card>
598 <CardHeader className="pb-2">
599 <CardTitle className="text-base">
600 {__("Status", "yatra")}
601 </CardTitle>
602 </CardHeader>
603 <CardContent>
604 <Select
605 id="status"
606 value={formData.status}
607 onChange={(e) =>
608 handleFieldChange("status", e.target.value)
609 }
610 className="w-full h-10"
611 >
612 <option value="draft">{__("Draft", "yatra")}</option>
613 <option value="publish">{__("Publish", "yatra")}</option>
614 <option value="trash">{__("Trash", "yatra")}</option>
615 </Select>
616 </CardContent>
617 </Card>
618
619 <Card>
620 <CardHeader className="pb-2">
621 <CardTitle className="text-base">
622 {__("Visibility", "yatra")}
623 </CardTitle>
624 </CardHeader>
625 <CardContent>
626 <label className="flex items-start gap-3 cursor-pointer">
627 <input
628 type="checkbox"
629 className="mt-1 h-4 w-4 rounded border-gray-300 text-blue-600 focus:ring-blue-500"
630 checked={formData.is_featured}
631 onChange={(e) =>
632 handleFieldChange("is_featured", e.target.checked)
633 }
634 />
635 <span>
636 <span className="block text-sm font-medium text-gray-900 dark:text-gray-100">
637 {__("Featured category", "yatra")}
638 </span>
639 <span className="block text-xs text-gray-500 dark:text-gray-400 mt-0.5">
640 {__(
641 "Shows a “Featured” badge on Trip Category blocks and the [yatra_trip_category] shortcode cards.",
642 "yatra",
643 )}
644 </span>
645 </span>
646 </label>
647 </CardContent>
648 </Card>
649
650 {/* SEO Settings */}
651 <Card>
652 <CardHeader className="pb-2">
653 <CardTitle className="text-base">
654 {__("SEO Settings", "yatra")}
655 </CardTitle>
656 </CardHeader>
657 <CardContent className="space-y-3">
658 <div>
659 <label
660 htmlFor="seo_title"
661 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
662 >
663 {__("Meta Title", "yatra")}
664 </label>
665 <Input
666 id="seo_title"
667 type="text"
668 value={formData.seo_title}
669 onChange={(e) =>
670 handleFieldChange("seo_title", e.target.value)
671 }
672 placeholder={__(
673 "e.g., {name} Trip Categories | Your Travel Agency",
674 "yatra",
675 )}
676 className="w-full"
677 />
678 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
679 {__(
680 "Custom title for search engines. Use {name} as placeholder.",
681 "yatra",
682 )}
683 </p>
684 </div>
685
686 <div>
687 <label
688 htmlFor="seo_description"
689 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
690 >
691 {__("Meta Description", "yatra")}
692 </label>
693 <textarea
694 id="seo_description"
695 value={formData.seo_description}
696 onChange={(e) =>
697 handleFieldChange("seo_description", e.target.value)
698 }
699 placeholder={__(
700 "e.g., Browse {name} trip categories and find your perfect adventure. Book now!",
701 "yatra",
702 )}
703 rows={3}
704 className="w-full px-3 py-2 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:text-white"
705 />
706 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
707 {__(
708 "Description for search engines (150-160 characters). Use {name} as placeholder.",
709 "yatra",
710 )}
711 </p>
712 </div>
713
714 <div>
715 <label
716 htmlFor="seo_keywords"
717 className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1"
718 >
719 {__("Meta Keywords", "yatra")}
720 </label>
721 <Input
722 id="seo_keywords"
723 type="text"
724 value={formData.seo_keywords}
725 onChange={(e) =>
726 handleFieldChange("seo_keywords", e.target.value)
727 }
728 placeholder={__(
729 "e.g., categories, trips, {name}, travel, adventure",
730 "yatra",
731 )}
732 className="w-full"
733 />
734 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
735 {__(
736 "Comma-separated keywords. Use {name} as placeholder.",
737 "yatra",
738 )}
739 </p>
740 </div>
741 </CardContent>
742 </Card>
743
744 <Card>
745 <CardHeader className="pb-2">
746 <CardTitle className="text-base">
747 {__("Category Icon or Image", "yatra")}
748 </CardTitle>
749 </CardHeader>
750 <CardContent>
751 <IconPicker
752 value={formData.icon}
753 onChange={(value) => handleFieldChange("icon", value)}
754 label={__("Select Icon or Upload Image", "yatra")}
755 helpText={__(
756 "Choose a library icon or upload a custom image for this category.",
757 "yatra",
758 )}
759 allowImageUpload={true}
760 allowIconSelection={true}
761 size="md"
762 />
763 </CardContent>
764 </Card>
765
766 <Card>
767 <CardContent className="p-3">
768 <div className="space-y-2">
769 <div className="flex gap-2">
770 <Button
771 type="submit"
772 disabled={isSubmitting}
773 className="flex-1 flex items-center justify-center gap-2"
774 >
775 {isSubmitting ? (
776 <>
777 <Loader2 className="w-4 h-4 animate-spin" />
778 {__("Saving...", "yatra")}
779 </>
780 ) : (
781 <>
782 <Save className="w-4 h-4" />
783 {isEditMode
784 ? __("Update Category", "yatra")
785 : __("Create Category", "yatra")}
786 </>
787 )}
788 </Button>
789 <Button
790 type="button"
791 variant="outline"
792 onClick={handleCancel}
793 disabled={isSubmitting}
794 >
795 {__("Cancel", "yatra")}
796 </Button>
797 </div>
798 </div>
799 </CardContent>
800 </Card>
801 </div>
802 </div>
803 </form>
804 </ConditionalRender>
805 </div>
806 );
807 };
808
809 export default CategoryForm;
810