Skip to content
All Taste components

Settings

Profile settings

A settings page section: profile photo and fields, notification switches and a save bar.

label column · photo upload · fields · switch rows · unsaved-changes bar

Profile

How you appear to clients and your team.

AR

Notifications

Choose what we send you.

Booking remindersAn email the evening before each appointment.
Weekly summaryRevenue, bookings and no-shows every Monday.
Product updatesNew features, at most once a month.
You have unsaved changes
"use client";
import { useState } from "react";
import { Upload } from "lucide-react";

const input = "h-9 w-full rounded border border-border-strong bg-bg px-3 text-sm text-fg outline-none focus:border-primary focus:ring-[3px] focus:ring-primary/20";

function Field({ label, help, children }: { label: string; help?: string; children: React.ReactNode }) {
  return (
    <label className="grid gap-1.5">
      <span className="text-[13px] font-medium">{label}</span>
      {children}
      {help && <span className="text-xs text-fg-subtle">{help}</span>}
    </label>
  );
}

function Section({ title, hint, children }: { title: string; hint: string; children: React.ReactNode }) {
  return (
    <div className="flex flex-wrap gap-x-8 gap-y-3 border-t border-border p-5 first:border-t-0">
      <div className="min-w-0 flex-[1_1_180px]">
        <h3 className="text-sm font-medium">{title}</h3>
        <p className="mt-0.5 text-[12.5px] text-fg-subtle">{hint}</p>
      </div>
      <div className="grid min-w-0 flex-[2_1_280px] gap-3.5">{children}</div>
    </div>
  );
}

function Toggle({ label, hint, checked, onChange }: { label: string; hint: string; checked: boolean; onChange: (v: boolean) => void }) {
  return (
    <div className="flex items-center justify-between gap-4 border-t border-border py-3 first:border-t-0 first:pt-0">
      <div>
        <p className="text-[13.5px] font-medium">{label}</p>
        <p className="text-[12.5px] text-fg-subtle">{hint}</p>
      </div>
      <button type="button" role="switch" aria-checked={checked} aria-label={label} onClick={() => onChange(!checked)}
        className="relative h-[22px] w-[38px] shrink-0 rounded-full bg-border-strong transition-colors aria-checked:bg-primary">
        <span className="absolute left-[3px] top-[3px] size-4 rounded-full bg-bg shadow-sm transition-[left] [[aria-checked=true]>&]:left-[19px]" />
      </button>
    </div>
  );
}

const initial = {
  name: "Asha Raman", phone: "+91 98450 12345", email: "asha@saffronstudio.in", tz: "ist",
  reminders: true, summary: true, updates: false,
};

export function ProfileSettings({ onSave }: { onSave?: (v: typeof initial) => void }) {
  const [saved, setSaved] = useState(initial);
  const [v, setV] = useState(initial);
  const dirty = JSON.stringify(v) !== JSON.stringify(saved);
  const set = <K extends keyof typeof initial>(k: K, val: (typeof initial)[K]) => setV((s) => ({ ...s, [k]: val }));

  return (
    <form className="w-full overflow-hidden rounded-lg border border-border bg-bg"
      onSubmit={(e) => { e.preventDefault(); onSave?.(v); setSaved(v); }}>
      <Section title="Profile" hint="How you appear to clients and your team.">
        <div className="flex flex-wrap items-center gap-3">
          <span className="grid size-12 place-items-center rounded-full bg-primary-soft font-medium text-primary-soft-fg">AR</span>
          <button type="button" className="inline-flex h-[30px] items-center gap-1.5 rounded border border-border-strong px-2.5 text-[13px] font-medium hover:bg-surface">
            <Upload className="size-3.5" aria-hidden /> Change photo
          </button>
          <button type="button" className="h-[30px] rounded px-2.5 text-[13px] font-medium hover:bg-surface-2">Remove</button>
        </div>
        <div className="grid gap-3.5 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
          <Field label="Full name"><input className={input} value={v.name} onChange={(e) => set("name", e.target.value)} /></Field>
          <Field label="Phone"><input className={input + " tabular-nums"} type="tel" value={v.phone} onChange={(e) => set("phone", e.target.value)} /></Field>
        </div>
        <Field label="Email" help="Receipts and sign-in links go here.">
          <input className={input} type="email" value={v.email} onChange={(e) => set("email", e.target.value)} />
        </Field>
        <Field label="Time zone">
          <select className={input + " appearance-none"} value={v.tz} onChange={(e) => set("tz", e.target.value)}>
            <option value="ist">(GMT+5:30) India Standard Time</option>
            <option value="gst">(GMT+4:00) Gulf Standard Time</option>
            <option value="sgt">(GMT+8:00) Singapore Time</option>
          </select>
        </Field>
      </Section>

      <Section title="Notifications" hint="Choose what we send you.">
        <div>
          <Toggle label="Booking reminders" hint="An email the evening before each appointment." checked={v.reminders} onChange={(x) => set("reminders", x)} />
          <Toggle label="Weekly summary" hint="Revenue, bookings and no-shows every Monday." checked={v.summary} onChange={(x) => set("summary", x)} />
          <Toggle label="Product updates" hint="New features, at most once a month." checked={v.updates} onChange={(x) => set("updates", x)} />
        </div>
      </Section>

      {dirty && (
        <div className="flex flex-wrap items-center justify-between gap-3 border-t border-border bg-surface px-5 py-3">
          <span className="text-[13px] text-fg-muted">You have unsaved changes</span>
          <div className="flex gap-2">
            <button type="button" onClick={() => setV(saved)} className="h-[30px] rounded px-2.5 text-[13px] font-medium hover:bg-surface-2">Discard</button>
            <button type="submit" className="h-[30px] rounded bg-primary px-2.5 text-[13px] font-medium text-primary-fg hover:bg-primary-hover">Save changes</button>
          </div>
        </div>
      )}
    </form>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.