PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.9
Yatra – Travel Booking & Tour Operator Software v3.0.9
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 / account / Profile.tsx

Profile.tsx in Yatra – Travel Booking & Tour Operator Software 3.0.9, at resources/js/pages/account/Profile.tsx

521 lines 21.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 import React, { useState, useEffect } from "react";
2 import {
3 User,
4 PenSquare,
5 ShieldCheck,
6 Heart,
7 CheckCircle,
8 XCircle,
9 } from "lucide-react";
10 import { useQueryClient } from "@tanstack/react-query";
11 import { __ } from "../../lib/i18n";
12 import { currency } from "./utils";
13 import { getCountryName, getCountryOptions } from "../../lib/countries";
14 import { apiClient } from "../../lib/api-client";
15 import { API_ENDPOINTS } from "../../lib/api-endpoints";
16 import { useToast } from "../../components/ui/toast";
17 import type { CustomerProfile } from "./types";
18
19 interface ProfileProps {
20 profile: CustomerProfile | null;
21 savedTrips: any[];
22 wishlistEnabled?: boolean;
23 }
24
25 const Profile: React.FC<ProfileProps> = ({
26 profile,
27 savedTrips,
28 wishlistEnabled = false,
29 }) => {
30 const [isEditing, setIsEditing] = useState(false);
31 const [formData, setFormData] = useState({
32 name: "",
33 email: "",
34 phone: "",
35 address: "",
36 city: "",
37 country: "",
38 });
39
40 const [isChangingPassword, setIsChangingPassword] = useState(false);
41 const [passwordData, setPasswordData] = useState({
42 currentPassword: "",
43 newPassword: "",
44 confirmPassword: "",
45 });
46
47 // Update formData when profile changes
48 useEffect(() => {
49 if (profile && !isEditing) {
50 setFormData({
51 name: profile.name || "",
52 email: profile.email || "",
53 phone: profile.phone || "",
54 address: profile.address || "",
55 city: profile.city || "",
56 country: profile.country || "",
57 });
58 }
59 }, [profile, isEditing]);
60
61 const queryClient = useQueryClient();
62 const { showToast } = useToast();
63 const [isSaving, setIsSaving] = useState(false);
64 const [isSavingPassword, setIsSavingPassword] = useState(false);
65
66 const handleInputChange = (field: string, value: string) => {
67 setFormData((prev) => ({ ...prev, [field]: value }));
68 };
69
70 const handleSave = async () => {
71 setIsSaving(true);
72 try {
73 // The form has a single "Name" field; the backend stores first/last
74 // separately. Split on whitespace (first token = first name, rest = last).
75 const parts = formData.name.trim().split(/\s+/).filter(Boolean);
76 const firstName = parts.shift() || "";
77 const lastName = parts.join(" ");
78
79 // Email is intentionally NOT sent — it is the account login and is
80 // locked (the backend also strips it defensively).
81 const payload: Record<string, string> = {
82 first_name: firstName,
83 phone: formData.phone,
84 address: formData.address,
85 city: formData.city,
86 country: formData.country,
87 };
88 // Only send last_name when present (backend rejects an empty last_name).
89 if (lastName) {
90 payload.last_name = lastName;
91 }
92
93 await apiClient.put(API_ENDPOINTS.CUSTOMER_ME, payload);
94
95 // Refetch the parent's profile query so the saved values are reflected.
96 await queryClient.invalidateQueries({ queryKey: ["account-profile"] });
97
98 showToast(__("Profile updated successfully.", "yatra"), "success");
99 setIsEditing(false);
100 } catch (error: any) {
101 showToast(
102 error?.message || __("Failed to update profile.", "yatra"),
103 "error",
104 );
105 } finally {
106 setIsSaving(false);
107 }
108 };
109
110 const handleChangePassword = async () => {
111 if (passwordData.newPassword !== passwordData.confirmPassword) {
112 showToast(__("Passwords do not match.", "yatra"), "error");
113 return;
114 }
115
116 setIsSavingPassword(true);
117 try {
118 await apiClient.post(API_ENDPOINTS.CUSTOMER_CHANGE_PASSWORD, {
119 current_password: passwordData.currentPassword,
120 new_password: passwordData.newPassword,
121 });
122
123 showToast(__("Password changed successfully.", "yatra"), "success");
124 setIsChangingPassword(false);
125 setPasswordData({
126 currentPassword: "",
127 newPassword: "",
128 confirmPassword: "",
129 });
130 // Changing the password rotates the WP auth cookie + REST nonce, so the
131 // current page's nonce is now stale. Reload to pick up a fresh one and
132 // avoid 403s on subsequent actions.
133 setTimeout(() => window.location.reload(), 1200);
134 } catch (error: any) {
135 showToast(
136 error?.message || __("Failed to change password.", "yatra"),
137 "error",
138 );
139 } finally {
140 setIsSavingPassword(false);
141 }
142 };
143
144 const handleCancel = () => {
145 setIsEditing(false);
146 if (profile) {
147 setFormData({
148 name: profile.name || "",
149 email: profile.email || "",
150 phone: profile.phone || "",
151 address: profile.address || "",
152 city: profile.city || "",
153 country: profile.country || "",
154 });
155 }
156 };
157
158 return (
159 <div className="yatra-profile-page space-y-6">
160 {/* Header */}
161 <div className="yatra-profile-header bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
162 <div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
163 <div>
164 <h2 className="text-lg font-bold text-gray-900 dark:text-white flex items-center gap-3">
165 <div className="p-2 bg-yatra-soft dark:bg-yatra-surface-dark-muted rounded-lg">
166 <User className="w-6 h-6 text-yatra-primary dark:text-yatra-on-dark" />
167 </div>
168 {__("Profile", "yatra")}
169 </h2>
170 <p className="text-sm text-gray-500 dark:text-gray-400 mt-2">
171 {__(
172 "Update your contact details, address, and communication preferences.",
173 "yatra",
174 )}
175 </p>
176 </div>
177 {!isEditing && (
178 <div
179 role="button"
180 tabIndex={0}
181 onClick={() => setIsEditing(true)}
182 className="yatra-profile-edit-btn inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium cursor-pointer"
183 >
184 <PenSquare className="w-4 h-4" /> {__("Edit Profile", "yatra")}
185 </div>
186 )}
187 </div>
188 </div>
189
190 {/* Profile Form */}
191 <div className="yatra-profile-card bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
192 <div className="yatra-profile-fields space-y-6">
193 <div className="grid gap-6 md:grid-cols-2">
194 <div className="yatra-profile-field">
195 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
196 {__("Full Name", "yatra")}{" "}
197 <span className="text-red-500">*</span>
198 </label>
199 {isEditing ? (
200 <input
201 type="text"
202 value={formData.name}
203 onChange={(e) => handleInputChange("name", e.target.value)}
204 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
205 placeholder={__("Enter your full name", "yatra")}
206 />
207 ) : (
208 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
209 {formData.name || __("Not set", "yatra")}
210 </p>
211 )}
212 </div>
213
214 <div className="yatra-profile-field">
215 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
216 {__("Email Address", "yatra")}
217 </label>
218 {/* Email is the account login and is never editable here —
219 always shown as static text, even in edit mode. */}
220 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
221 {formData.email || __("Not set", "yatra")}
222 </p>
223 </div>
224
225 <div className="yatra-profile-field">
226 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
227 {__("Phone Number", "yatra")}
228 </label>
229 {isEditing ? (
230 <input
231 type="tel"
232 value={formData.phone}
233 onChange={(e) => handleInputChange("phone", e.target.value)}
234 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
235 placeholder={__("Enter your phone number", "yatra")}
236 />
237 ) : (
238 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
239 {formData.phone || __("Not set", "yatra")}
240 </p>
241 )}
242 </div>
243
244 {/* Logical order: Country → City → Address (broad to specific). */}
245 <div className="yatra-profile-field">
246 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
247 {__("Country", "yatra")}
248 </label>
249 {isEditing ? (
250 <select
251 value={formData.country}
252 onChange={(e) => handleInputChange("country", e.target.value)}
253 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
254 >
255 <option value="">{__("Select your country", "yatra")}</option>
256 {getCountryOptions().map((c) => (
257 <option key={c.code} value={c.code}>
258 {c.name}
259 </option>
260 ))}
261 </select>
262 ) : (
263 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
264 {getCountryName(formData.country) || __("Not set", "yatra")}
265 </p>
266 )}
267 </div>
268
269 <div className="yatra-profile-field">
270 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
271 {__("City", "yatra")}
272 </label>
273 {isEditing ? (
274 <input
275 type="text"
276 value={formData.city}
277 onChange={(e) => handleInputChange("city", e.target.value)}
278 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
279 placeholder={__("Enter your city", "yatra")}
280 />
281 ) : (
282 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
283 {formData.city || __("Not set", "yatra")}
284 </p>
285 )}
286 </div>
287
288 <div className="yatra-profile-field md:col-span-2">
289 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
290 {__("Address", "yatra")}
291 </label>
292 {isEditing ? (
293 <textarea
294 value={formData.address}
295 onChange={(e) => handleInputChange("address", e.target.value)}
296 rows={3}
297 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
298 placeholder={__("Enter your address", "yatra")}
299 />
300 ) : (
301 <p className="text-sm font-medium text-gray-900 dark:text-white py-2">
302 {formData.address || __("Not set", "yatra")}
303 </p>
304 )}
305 </div>
306 </div>
307
308 {isEditing && (
309 <div className="yatra-profile-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
310 <button
311 type="button"
312 onClick={handleSave}
313 disabled={isSaving}
314 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
315 >
316 <CheckCircle className="w-4 h-4" />
317 {isSaving
318 ? __("Saving…", "yatra")
319 : __("Save Changes", "yatra")}
320 </button>
321 <div
322 role="button"
323 tabIndex={0}
324 onClick={handleCancel}
325 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium cursor-pointer"
326 >
327 <XCircle className="w-4 h-4" />
328 {__("Cancel", "yatra")}
329 </div>
330 </div>
331 )}
332 </div>
333 </div>
334
335 {/* Password Change Section */}
336 <div className="yatra-profile-password bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
337 <div className="flex items-center justify-between mb-6">
338 <div>
339 <h3 className="text-base font-semibold text-gray-900 dark:text-white flex items-center gap-2">
340 <ShieldCheck className="w-5 h-5 text-yatra-primary dark:text-yatra-on-dark" />
341 {__("Change Password", "yatra")}
342 </h3>
343 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
344 {__("Update your password to keep your account secure.", "yatra")}
345 </p>
346 </div>
347 {!isChangingPassword && (
348 <div
349 role="button"
350 tabIndex={0}
351 onClick={() => setIsChangingPassword(true)}
352 className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium cursor-pointer"
353 >
354 <ShieldCheck className="w-4 h-4" />
355 {__("Change Password", "yatra")}
356 </div>
357 )}
358 </div>
359
360 {isChangingPassword && (
361 <div className="space-y-4">
362 <div className="yatra-password-field">
363 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
364 {__("Current Password", "yatra")}{" "}
365 <span className="text-red-500">*</span>
366 </label>
367 <input
368 type="password"
369 value={passwordData.currentPassword}
370 onChange={(e) =>
371 setPasswordData((prev) => ({
372 ...prev,
373 currentPassword: e.target.value,
374 }))
375 }
376 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
377 placeholder={__("Enter your current password", "yatra")}
378 />
379 </div>
380
381 <div className="yatra-password-field">
382 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
383 {__("New Password", "yatra")}{" "}
384 <span className="text-red-500">*</span>
385 </label>
386 <input
387 type="password"
388 value={passwordData.newPassword}
389 onChange={(e) =>
390 setPasswordData((prev) => ({
391 ...prev,
392 newPassword: e.target.value,
393 }))
394 }
395 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
396 placeholder={__("Enter new password", "yatra")}
397 />
398 </div>
399
400 <div className="yatra-password-field">
401 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
402 {__("Confirm New Password", "yatra")}{" "}
403 <span className="text-red-500">*</span>
404 </label>
405 <input
406 type="password"
407 value={passwordData.confirmPassword}
408 onChange={(e) =>
409 setPasswordData((prev) => ({
410 ...prev,
411 confirmPassword: e.target.value,
412 }))
413 }
414 className="w-full rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-white px-4 py-2 text-sm focus:ring-2 focus:ring-yatra-primary focus:border-transparent"
415 placeholder={__("Confirm your new password", "yatra")}
416 />
417 {passwordData.newPassword &&
418 passwordData.confirmPassword &&
419 passwordData.newPassword !== passwordData.confirmPassword && (
420 <p className="text-xs text-red-500 mt-1">
421 {__("Passwords do not match", "yatra")}
422 </p>
423 )}
424 </div>
425
426 <div className="yatra-password-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
427 <button
428 type="button"
429 onClick={handleChangePassword}
430 disabled={
431 isSavingPassword ||
432 !passwordData.currentPassword ||
433 !passwordData.newPassword ||
434 !passwordData.confirmPassword ||
435 passwordData.newPassword !== passwordData.confirmPassword
436 }
437 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg bg-yatra-primary text-white hover:bg-yatra-primary-dark transition-colors text-sm font-medium disabled:opacity-50 disabled:cursor-not-allowed"
438 >
439 <CheckCircle className="w-4 h-4" />
440 {isSavingPassword
441 ? __("Updating…", "yatra")
442 : __("Update Password", "yatra")}
443 </button>
444 <div
445 role="button"
446 tabIndex={0}
447 onClick={() => {
448 setIsChangingPassword(false);
449 setPasswordData({
450 currentPassword: "",
451 newPassword: "",
452 confirmPassword: "",
453 });
454 }}
455 className="inline-flex items-center gap-2 px-6 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors text-sm font-medium cursor-pointer"
456 >
457 <XCircle className="w-4 h-4" />
458 {__("Cancel", "yatra")}
459 </div>
460 </div>
461 </div>
462 )}
463 </div>
464
465 {/* Additional Sections.
466 Communication Preferences card was removed: it shipped as four
467 checkboxes with `defaultChecked` and no onChange / no save
468 handler / no API persistence — purely decorative. Showing
469 fake toggles is worse than not shipping the feature because
470 customers expect a "Save" round-trip and refresh-revert.
471 Re-introduce only when there's a real preferences endpoint
472 + service-side honouring (suppress emails when toggled off). */}
473 <div className="yatra-profile-sections grid gap-6 lg:grid-cols-1">
474 {wishlistEnabled && (
475 <div className="yatra-profile-saved-trips bg-white dark:bg-gray-800 border border-gray-100 dark:border-gray-700 rounded-xl shadow-sm p-6">
476 <h3 className="text-base font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
477 <Heart className="w-5 h-5 text-red-600 dark:text-red-400" />
478 {__("Saved Trips", "yatra")}
479 </h3>
480 <div className="yatra-saved-trips-list space-y-3">
481 {savedTrips.length > 0 ? (
482 savedTrips.map((trip: any) => (
483 <div
484 key={trip.id || trip.trip_id}
485 className="yatra-saved-trip-item flex items-center justify-between border border-gray-100 dark:border-gray-700 rounded-lg p-4 hover:bg-gray-50 dark:hover:bg-gray-900/40 transition-colors"
486 >
487 <div className="flex-1 min-w-0">
488 <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
489 {trip.trip_title || trip.title}
490 </p>
491 {(trip.location || trip.duration) && (
492 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
493 {trip.location || trip.duration}
494 </p>
495 )}
496 </div>
497 <div className="text-right ml-4 flex-shrink-0">
498 <p className="text-xs text-gray-500 dark:text-gray-400">
499 {__("From", "yatra")}
500 </p>
501 <p className="text-sm font-semibold text-gray-900 dark:text-white">
502 {currency(trip.price_from || trip.price || 0)}
503 </p>
504 </div>
505 </div>
506 ))
507 ) : (
508 <p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
509 {__("No saved trips yet", "yatra")}
510 </p>
511 )}
512 </div>
513 </div>
514 )}
515 </div>
516 </div>
517 );
518 };
519
520 export default Profile;
521