Some checks failed
Pull Request Checks / Code Quality & Security (push) Has been cancelled
Security Audit / Security Vulnerability Audit (push) Has been cancelled
Security Audit / Dependency Review (push) Has been cancelled
Security Audit / CodeQL Security Analysis (push) Has been cancelled
Security Audit / Check Outdated Dependencies (push) Has been cancelled
- Migrate all molecule components (DataTable, PaginationBar, FilterDropdown, AlertBanner, FormField, SectionCard, SubCard, MetricCard, AnimatedCard, OtpInput) to shadcn/ui primitives with legacy backups and comparison stories - Install 24 shadcn/ui primitives (accordion, alert, badge, button, card, checkbox, collapsible, dialog, dropdown-menu, input-otp, input, label, pagination, popover, radio-group, select, separator, sheet, skeleton, table, tabs, toggle-group, toggle, tooltip) with barrel exports - Replace 69 raw HTML elements across all features with shadcn components: 35+ <button> → Button, 5 <select> → Select, 15+ <label> → Label, 6 <input type=checkbox> → Checkbox, 7 <input type=radio> → RadioGroup - Add TextRotate animation component and integrate into hero section with rotating service names (Internet, Phone Plans, VPN, IT Support, Business) - Add destructive color token aliases for error state consistency - Add CLAUDE.md rules for shadcn migration process Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
225 lines
6.4 KiB
TypeScript
225 lines
6.4 KiB
TypeScript
"use client";
|
|
|
|
import { UserIcon, PencilIcon, CheckIcon, XMarkIcon } from "@heroicons/react/24/outline";
|
|
import { Button } from "@/components/atoms/button";
|
|
import { Input } from "@/components/atoms/input";
|
|
import { Label } from "@/components/atoms/label";
|
|
|
|
/** Data required for displaying personal info card */
|
|
interface PersonalInfoData {
|
|
firstname: string | null | undefined;
|
|
lastname: string | null | undefined;
|
|
email: string;
|
|
phonenumber: string | null | undefined;
|
|
sfNumber: string | null | undefined;
|
|
dateOfBirth: string | null | undefined;
|
|
gender: string | null | undefined;
|
|
}
|
|
|
|
interface PersonalInfoCardProps {
|
|
/** User profile data including read-only fields */
|
|
data: PersonalInfoData;
|
|
/** Email value for editing (may differ from data.email during edit) */
|
|
editEmail: string;
|
|
/** Phone number value for editing (may differ from data.phonenumber during edit) */
|
|
editPhoneNumber: string;
|
|
isEditing: boolean;
|
|
isSaving: boolean;
|
|
onEdit: () => void;
|
|
onCancel: () => void;
|
|
onChange: (field: "email" | "phonenumber", value: string) => void;
|
|
onSave: () => void;
|
|
}
|
|
|
|
function ReadOnlyField({
|
|
label,
|
|
value,
|
|
hint,
|
|
}: {
|
|
label: string;
|
|
value: string | null | undefined;
|
|
hint: string;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<Label className="block text-sm font-medium text-muted-foreground mb-2">{label}</Label>
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
|
<p className="text-base text-foreground font-medium">
|
|
{value || <span className="text-muted-foreground italic">Not provided</span>}
|
|
</p>
|
|
<p className="text-xs text-muted-foreground mt-2">{hint}</p>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const EDITABLE_INPUT_CLASS =
|
|
"block w-full px-4 py-2.5 border border-input rounded-lg bg-background text-foreground shadow-[var(--cp-shadow-1)] focus:outline-none focus:ring-2 focus:ring-ring focus:border-ring transition-colors";
|
|
|
|
function EditableEmailField({
|
|
email,
|
|
editEmail,
|
|
isEditing,
|
|
onChange,
|
|
}: {
|
|
email: string;
|
|
editEmail: string;
|
|
isEditing: boolean;
|
|
onChange: (field: "email" | "phonenumber", value: string) => void;
|
|
}) {
|
|
return (
|
|
<div className="sm:col-span-2">
|
|
<Label className="block text-sm font-medium text-muted-foreground mb-2">Email Address</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
type="email"
|
|
value={editEmail}
|
|
onChange={e => onChange("email", e.target.value)}
|
|
className={EDITABLE_INPUT_CLASS}
|
|
/>
|
|
) : (
|
|
<div className="bg-card rounded-lg p-4 border border-border shadow-sm">
|
|
<p className="text-base text-foreground font-medium">{email}</p>
|
|
<p className="text-xs text-muted-foreground mt-2">
|
|
Email can be updated from the portal.
|
|
</p>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function EditablePhoneField({
|
|
phonenumber,
|
|
editPhoneNumber,
|
|
isEditing,
|
|
onChange,
|
|
}: {
|
|
phonenumber: string | null | undefined;
|
|
editPhoneNumber: string;
|
|
isEditing: boolean;
|
|
onChange: (field: "email" | "phonenumber", value: string) => void;
|
|
}) {
|
|
return (
|
|
<div>
|
|
<Label className="block text-sm font-medium text-muted-foreground mb-2">Phone Number</Label>
|
|
{isEditing ? (
|
|
<Input
|
|
type="tel"
|
|
value={editPhoneNumber}
|
|
onChange={e => onChange("phonenumber", e.target.value)}
|
|
placeholder="+81 XX-XXXX-XXXX"
|
|
className={EDITABLE_INPUT_CLASS}
|
|
/>
|
|
) : (
|
|
<p className="text-base text-foreground py-2">
|
|
{phonenumber || <span className="text-muted-foreground italic">Not provided</span>}
|
|
</p>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export function PersonalInfoCard({
|
|
data,
|
|
editEmail,
|
|
editPhoneNumber,
|
|
isEditing,
|
|
isSaving,
|
|
onEdit,
|
|
onCancel,
|
|
onChange,
|
|
onSave,
|
|
}: PersonalInfoCardProps) {
|
|
return (
|
|
<div className="bg-card text-card-foreground rounded-xl border border-border shadow-[var(--cp-shadow-1)]">
|
|
<div className="px-6 py-5 border-b border-border">
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center space-x-3">
|
|
<UserIcon className="h-6 w-6 text-primary" />
|
|
<h2 className="text-xl font-semibold text-foreground">Personal Information</h2>
|
|
</div>
|
|
{!isEditing && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={onEdit}
|
|
leftIcon={<PencilIcon className="h-4 w-4" />}
|
|
>
|
|
Edit
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="p-6">
|
|
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2">
|
|
<ReadOnlyField
|
|
label="First Name"
|
|
value={data.firstname}
|
|
hint="Name cannot be changed from the portal."
|
|
/>
|
|
<ReadOnlyField
|
|
label="Last Name"
|
|
value={data.lastname}
|
|
hint="Name cannot be changed from the portal."
|
|
/>
|
|
|
|
<EditableEmailField
|
|
email={data.email}
|
|
editEmail={editEmail}
|
|
isEditing={isEditing}
|
|
onChange={onChange}
|
|
/>
|
|
|
|
<ReadOnlyField
|
|
label="Customer Number"
|
|
value={data.sfNumber}
|
|
hint="Customer number is read-only."
|
|
/>
|
|
<ReadOnlyField
|
|
label="Date of Birth"
|
|
value={data.dateOfBirth}
|
|
hint="Date of birth is stored in billing profile."
|
|
/>
|
|
|
|
<EditablePhoneField
|
|
phonenumber={data.phonenumber}
|
|
editPhoneNumber={editPhoneNumber}
|
|
isEditing={isEditing}
|
|
onChange={onChange}
|
|
/>
|
|
|
|
<ReadOnlyField
|
|
label="Gender"
|
|
value={data.gender}
|
|
hint="Gender is stored in billing profile."
|
|
/>
|
|
</div>
|
|
|
|
{isEditing && (
|
|
<div className="flex items-center justify-end space-x-3 pt-6 border-t border-border mt-6">
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={onCancel}
|
|
disabled={isSaving}
|
|
leftIcon={<XMarkIcon className="h-4 w-4" />}
|
|
>
|
|
Cancel
|
|
</Button>
|
|
<Button
|
|
size="sm"
|
|
onClick={onSave}
|
|
isLoading={isSaving}
|
|
leftIcon={isSaving ? undefined : <CheckIcon className="h-4 w-4" />}
|
|
>
|
|
{isSaving ? "Saving..." : "Save Changes"}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|