PluginProbe
Yatra – Travel Booking & Tour Operator Software / 3.0.8
Yatra – Travel Booking & Tour Operator Software v3.0.8
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.8, at resources/js/pages/account/Profile.tsx

519 lines 21.8 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 ? __("Saving…", "yatra") : __("Save Changes", "yatra")}
318 </button>
319 <div
320 role="button"
321 tabIndex={0}
322 onClick={handleCancel}
323 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"
324 >
325 <XCircle className="w-4 h-4" />
326 {__("Cancel", "yatra")}
327 </div>
328 </div>
329 )}
330 </div>
331 </div>
332
333 {/* Password Change Section */}
334 <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">
335 <div className="flex items-center justify-between mb-6">
336 <div>
337 <h3 className="text-base font-semibold text-gray-900 dark:text-white flex items-center gap-2">
338 <ShieldCheck className="w-5 h-5 text-yatra-primary dark:text-yatra-on-dark" />
339 {__("Change Password", "yatra")}
340 </h3>
341 <p className="text-sm text-gray-500 dark:text-gray-400 mt-1">
342 {__("Update your password to keep your account secure.", "yatra")}
343 </p>
344 </div>
345 {!isChangingPassword && (
346 <div
347 role="button"
348 tabIndex={0}
349 onClick={() => setIsChangingPassword(true)}
350 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"
351 >
352 <ShieldCheck className="w-4 h-4" />
353 {__("Change Password", "yatra")}
354 </div>
355 )}
356 </div>
357
358 {isChangingPassword && (
359 <div className="space-y-4">
360 <div className="yatra-password-field">
361 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
362 {__("Current Password", "yatra")}{" "}
363 <span className="text-red-500">*</span>
364 </label>
365 <input
366 type="password"
367 value={passwordData.currentPassword}
368 onChange={(e) =>
369 setPasswordData((prev) => ({
370 ...prev,
371 currentPassword: e.target.value,
372 }))
373 }
374 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"
375 placeholder={__("Enter your current password", "yatra")}
376 />
377 </div>
378
379 <div className="yatra-password-field">
380 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
381 {__("New Password", "yatra")}{" "}
382 <span className="text-red-500">*</span>
383 </label>
384 <input
385 type="password"
386 value={passwordData.newPassword}
387 onChange={(e) =>
388 setPasswordData((prev) => ({
389 ...prev,
390 newPassword: e.target.value,
391 }))
392 }
393 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"
394 placeholder={__("Enter new password", "yatra")}
395 />
396 </div>
397
398 <div className="yatra-password-field">
399 <label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
400 {__("Confirm New Password", "yatra")}{" "}
401 <span className="text-red-500">*</span>
402 </label>
403 <input
404 type="password"
405 value={passwordData.confirmPassword}
406 onChange={(e) =>
407 setPasswordData((prev) => ({
408 ...prev,
409 confirmPassword: e.target.value,
410 }))
411 }
412 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"
413 placeholder={__("Confirm your new password", "yatra")}
414 />
415 {passwordData.newPassword &&
416 passwordData.confirmPassword &&
417 passwordData.newPassword !== passwordData.confirmPassword && (
418 <p className="text-xs text-red-500 mt-1">
419 {__("Passwords do not match", "yatra")}
420 </p>
421 )}
422 </div>
423
424 <div className="yatra-password-actions flex flex-wrap gap-3 pt-4 border-t border-gray-200 dark:border-gray-700">
425 <button
426 type="button"
427 onClick={handleChangePassword}
428 disabled={
429 isSavingPassword ||
430 !passwordData.currentPassword ||
431 !passwordData.newPassword ||
432 !passwordData.confirmPassword ||
433 passwordData.newPassword !== passwordData.confirmPassword
434 }
435 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"
436 >
437 <CheckCircle className="w-4 h-4" />
438 {isSavingPassword
439 ? __("Updating…", "yatra")
440 : __("Update Password", "yatra")}
441 </button>
442 <div
443 role="button"
444 tabIndex={0}
445 onClick={() => {
446 setIsChangingPassword(false);
447 setPasswordData({
448 currentPassword: "",
449 newPassword: "",
450 confirmPassword: "",
451 });
452 }}
453 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"
454 >
455 <XCircle className="w-4 h-4" />
456 {__("Cancel", "yatra")}
457 </div>
458 </div>
459 </div>
460 )}
461 </div>
462
463 {/* Additional Sections.
464 Communication Preferences card was removed: it shipped as four
465 checkboxes with `defaultChecked` and no onChange / no save
466 handler / no API persistence — purely decorative. Showing
467 fake toggles is worse than not shipping the feature because
468 customers expect a "Save" round-trip and refresh-revert.
469 Re-introduce only when there's a real preferences endpoint
470 + service-side honouring (suppress emails when toggled off). */}
471 <div className="yatra-profile-sections grid gap-6 lg:grid-cols-1">
472 {wishlistEnabled && (
473 <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">
474 <h3 className="text-base font-semibold text-gray-900 dark:text-white mb-4 flex items-center gap-2">
475 <Heart className="w-5 h-5 text-red-600 dark:text-red-400" />
476 {__("Saved Trips", "yatra")}
477 </h3>
478 <div className="yatra-saved-trips-list space-y-3">
479 {savedTrips.length > 0 ? (
480 savedTrips.map((trip: any) => (
481 <div
482 key={trip.id || trip.trip_id}
483 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"
484 >
485 <div className="flex-1 min-w-0">
486 <p className="text-sm font-medium text-gray-900 dark:text-white truncate">
487 {trip.trip_title || trip.title}
488 </p>
489 {(trip.location || trip.duration) && (
490 <p className="text-xs text-gray-500 dark:text-gray-400 mt-1">
491 {trip.location || trip.duration}
492 </p>
493 )}
494 </div>
495 <div className="text-right ml-4 flex-shrink-0">
496 <p className="text-xs text-gray-500 dark:text-gray-400">
497 {__("From", "yatra")}
498 </p>
499 <p className="text-sm font-semibold text-gray-900 dark:text-white">
500 {currency(trip.price_from || trip.price || 0)}
501 </p>
502 </div>
503 </div>
504 ))
505 ) : (
506 <p className="text-sm text-gray-500 dark:text-gray-400 text-center py-4">
507 {__("No saved trips yet", "yatra")}
508 </p>
509 )}
510 </div>
511 </div>
512 )}
513 </div>
514 </div>
515 );
516 };
517
518 export default Profile;
519