|
@@ -2,45 +2,110 @@ import { useEffect, useState, type FormEvent } from 'react'
|
|
|
import { Link } from 'react-router-dom'
|
|
import { Link } from 'react-router-dom'
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
import { DashboardLayout } from '../../layouts/DashboardLayout'
|
|
import { DashboardLayout } from '../../layouts/DashboardLayout'
|
|
|
-import { usePageContext } from '../../shared/pageContext'
|
|
|
|
|
-import { getStoredStylistProfile, saveStoredStylistProfile, type StylistProfileDraft } from '../../shared/stylistProfileStore'
|
|
|
|
|
-import { getSimulationInviteCode } from '../../services/api/simulations'
|
|
|
|
|
|
|
+import { usePageContext, usePreviewUrl } from '../../shared/pageContext'
|
|
|
|
|
+import { uploadMedia } from '../../services/api/media'
|
|
|
|
|
+import {
|
|
|
|
|
+ getStylistMe,
|
|
|
|
|
+ updateStylistMe,
|
|
|
|
|
+ type StylistProfile,
|
|
|
|
|
+} from '../../services/api/stylist'
|
|
|
|
|
+import { DEFAULT_USER_AVATAR_URL } from '../../shared/uiAssets'
|
|
|
|
|
+
|
|
|
|
|
+const EXPERIENCE_YEARS = Array.from({ length: 30 }, (_, index) => index + 1)
|
|
|
|
|
+
|
|
|
|
|
+const emptyProfile: StylistProfile = {
|
|
|
|
|
+ id: '',
|
|
|
|
|
+ username: '',
|
|
|
|
|
+ email: '',
|
|
|
|
|
+ displayName: '',
|
|
|
|
|
+ slug: '',
|
|
|
|
|
+ photoUrl: '',
|
|
|
|
|
+ lead: '',
|
|
|
|
|
+ bio: '',
|
|
|
|
|
+ bookingUrl: '',
|
|
|
|
|
+ experienceYears: null,
|
|
|
|
|
+ rating: '',
|
|
|
|
|
+ reviewCount: 0,
|
|
|
|
|
+ specialties: '',
|
|
|
|
|
+ address: '',
|
|
|
|
|
+ hours: '',
|
|
|
|
|
+ inviteCode: '',
|
|
|
|
|
+ publishedTemplateCount: 0,
|
|
|
|
|
+ templates: [],
|
|
|
|
|
+}
|
|
|
|
|
|
|
|
export function StylistProfileManagePage() {
|
|
export function StylistProfileManagePage() {
|
|
|
const { locale, navigate, path, copy } = usePageContext()
|
|
const { locale, navigate, path, copy } = usePageContext()
|
|
|
const pageCopy = copy.profileManagePage
|
|
const pageCopy = copy.profileManagePage
|
|
|
- const [profile, setProfile] = useState<StylistProfileDraft>(() => getStoredStylistProfile(locale))
|
|
|
|
|
|
|
+ const [profile, setProfile] = useState<StylistProfile>(emptyProfile)
|
|
|
|
|
+ const [photoFile, setPhotoFile] = useState<File | null>(null)
|
|
|
const [saved, setSaved] = useState(false)
|
|
const [saved, setSaved] = useState(false)
|
|
|
- const [inviteCode, setInviteCode] = useState('--------')
|
|
|
|
|
|
|
+ const [saving, setSaving] = useState(false)
|
|
|
|
|
+ const [error, setError] = useState('')
|
|
|
|
|
+ const photoPreview = usePreviewUrl(photoFile, profile.photoUrl || DEFAULT_USER_AVATAR_URL)
|
|
|
|
|
+ const publicPath = profile.slug ? `/${locale}/profile/${profile.slug}` : `/${locale}/profile`
|
|
|
|
|
+ const yearUnit = pageCopy.preview.yearUnit
|
|
|
|
|
+ const inviteCode = profile.inviteCode || '--------'
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- getSimulationInviteCode()
|
|
|
|
|
- .then((data) => setInviteCode(data.inviteCode))
|
|
|
|
|
- .catch(() => setInviteCode(pageCopy.inviteLoadFailed))
|
|
|
|
|
- }, [pageCopy.inviteLoadFailed])
|
|
|
|
|
|
|
+ let cancelled = false
|
|
|
|
|
+ getStylistMe()
|
|
|
|
|
+ .then((data) => {
|
|
|
|
|
+ if (!cancelled) setProfile(data)
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(() => {
|
|
|
|
|
+ if (!cancelled) setError(pageCopy.errors.loadFailed)
|
|
|
|
|
+ })
|
|
|
|
|
+ return () => {
|
|
|
|
|
+ cancelled = true
|
|
|
|
|
+ }
|
|
|
|
|
+ }, [pageCopy.errors.loadFailed])
|
|
|
|
|
|
|
|
- useEffect(() => {
|
|
|
|
|
- setProfile(getStoredStylistProfile(locale))
|
|
|
|
|
- setSaved(false)
|
|
|
|
|
- }, [locale])
|
|
|
|
|
- const patch = (part: Partial<StylistProfileDraft>) => {
|
|
|
|
|
|
|
+ const patch = (part: Partial<StylistProfile>) => {
|
|
|
setSaved(false)
|
|
setSaved(false)
|
|
|
|
|
+ setError('')
|
|
|
setProfile((current) => ({ ...current, ...part }))
|
|
setProfile((current) => ({ ...current, ...part }))
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- const handlePhoto = (file?: File) => {
|
|
|
|
|
- if (!file) return
|
|
|
|
|
- const reader = new FileReader()
|
|
|
|
|
- reader.onload = () => patch({ photo: String(reader.result || '') })
|
|
|
|
|
- reader.readAsDataURL(file)
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- const handleSubmit = (event: FormEvent<HTMLFormElement>) => {
|
|
|
|
|
|
|
+ const handleSubmit = async (event: FormEvent<HTMLFormElement>) => {
|
|
|
event.preventDefault()
|
|
event.preventDefault()
|
|
|
- saveStoredStylistProfile(locale, profile)
|
|
|
|
|
- setSaved(true)
|
|
|
|
|
|
|
+ if (!profile.displayName.trim() || saving) return
|
|
|
|
|
+ setSaving(true)
|
|
|
|
|
+ setError('')
|
|
|
|
|
+ try {
|
|
|
|
|
+ let photoUrl = profile.photoUrl
|
|
|
|
|
+ if (photoFile) {
|
|
|
|
|
+ photoUrl = (await uploadMedia(photoFile, 'avatar')).url
|
|
|
|
|
+ }
|
|
|
|
|
+ const updated = await updateStylistMe({
|
|
|
|
|
+ displayName: profile.displayName.trim(),
|
|
|
|
|
+ slug: profile.slug.trim(),
|
|
|
|
|
+ photoUrl,
|
|
|
|
|
+ lead: profile.lead,
|
|
|
|
|
+ bio: profile.bio,
|
|
|
|
|
+ bookingUrl: profile.bookingUrl.trim(),
|
|
|
|
|
+ experienceYears: profile.experienceYears,
|
|
|
|
|
+ rating: profile.rating.trim(),
|
|
|
|
|
+ reviewCount: Number(profile.reviewCount) || 0,
|
|
|
|
|
+ specialties: profile.specialties,
|
|
|
|
|
+ address: profile.address,
|
|
|
|
|
+ hours: profile.hours,
|
|
|
|
|
+ })
|
|
|
|
|
+ setProfile(updated)
|
|
|
|
|
+ setPhotoFile(null)
|
|
|
|
|
+ setSaved(true)
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ setSaved(false)
|
|
|
|
|
+ setError(err instanceof Error ? err.message : pageCopy.errors.saveFailed)
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setSaving(false)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const experienceLabel = profile.experienceYears ? `${profile.experienceYears}${yearUnit}` : '—'
|
|
|
|
|
+ const fieldCopy = pageCopy.form.fields
|
|
|
|
|
+ const sections = pageCopy.form.sections
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<DashboardLayout>
|
|
<DashboardLayout>
|
|
|
<Helmet><html lang={locale === 'ja' ? 'ja' : locale === 'en' ? 'en' : 'zh-CN'} /><title>{`Aitori Stylist | ${pageCopy.title}`}</title></Helmet>
|
|
<Helmet><html lang={locale === 'ja' ? 'ja' : locale === 'en' ? 'en' : 'zh-CN'} /><title>{`Aitori Stylist | ${pageCopy.title}`}</title></Helmet>
|
|
@@ -49,8 +114,8 @@ export function StylistProfileManagePage() {
|
|
|
<div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
|
|
<div className="d-flex align-items-center justify-content-between flex-wrap gap-3 mb-4">
|
|
|
<Link to={path('/dashboard')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>{pageCopy.back}</Link>
|
|
<Link to={path('/dashboard')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-arrow-left me-6px"></i>{pageCopy.back}</Link>
|
|
|
<div className="d-flex flex-wrap gap-2">
|
|
<div className="d-flex flex-wrap gap-2">
|
|
|
- <Link to={path('/profile')} target="_blank" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-external-link me-8px"></i>{pageCopy.viewPublic}</Link>
|
|
|
|
|
- <button type="submit" form="stylist-profile-form" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-save me-10px"></i>{copy.common.save}</button>
|
|
|
|
|
|
|
+ <Link to={path(profile.slug ? `/profile/${profile.slug}` : '/profile')} target="_blank" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-external-link me-8px"></i>{pageCopy.viewPublic}</Link>
|
|
|
|
|
+ <button type="submit" form="stylist-profile-form" disabled={saving} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-save me-8px"></i>{saving ? copy.common.sending : copy.common.save}</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
<div className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
@@ -67,33 +132,97 @@ export function StylistProfileManagePage() {
|
|
|
<aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
<aside className="workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
|
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.preview.badge}</span>
|
|
<span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.preview.badge}</span>
|
|
|
<div className="stylist-profile-manage-card mt-20px">
|
|
<div className="stylist-profile-manage-card mt-20px">
|
|
|
- <img src={profile.photo} alt={pageCopy.preview.photoAlt.replace('{{name}}', profile.displayName)} />
|
|
|
|
|
- <div><span className="template-status-badge">{pageCopy.preview.published}</span><h2>{profile.displayName}</h2><p>{profile.lead}</p><div className="stylist-profile-manage-stats"><span><strong>{profile.years}</strong>{pageCopy.preview.experience}</span><span><strong>{profile.rating}</strong>{pageCopy.preview.rating}</span><span><strong>{profile.reviewCount}</strong>{pageCopy.preview.reviews}</span></div></div>
|
|
|
|
|
|
|
+ <img src={photoPreview} alt={pageCopy.preview.photoAlt.replace('{{name}}', profile.displayName || pageCopy.preview.untitled)} />
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <span className="template-status-badge">{pageCopy.preview.published}</span>
|
|
|
|
|
+ <h2>{profile.displayName || pageCopy.preview.untitled}</h2>
|
|
|
|
|
+ <p>{profile.lead || pageCopy.preview.emptyLead}</p>
|
|
|
|
|
+ <div className="stylist-profile-manage-stats">
|
|
|
|
|
+ <span><strong>{experienceLabel}</strong>{pageCopy.preview.experience}</span>
|
|
|
|
|
+ <span><strong>{profile.rating || '—'}</strong>{pageCopy.preview.rating}</span>
|
|
|
|
|
+ <span><strong>{profile.reviewCount || '—'}</strong>{pageCopy.preview.reviews}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="template-detail-summary mt-25px">
|
|
|
|
|
+ <div><span>{pageCopy.summary.publicUrl}</span><strong>{publicPath}</strong></div>
|
|
|
|
|
+ <div><span>{pageCopy.summary.inviteCode}</span><strong>{inviteCode}</strong></div>
|
|
|
|
|
+ <div><span>{pageCopy.summary.booking}</span><strong>{profile.bookingUrl ? pageCopy.summary.configured : pageCopy.summary.notConfigured}</strong></div>
|
|
|
|
|
+ <div><span>{pageCopy.summary.templates}</span><strong>{pageCopy.summary.templateCount.replace('{{count}}', String(profile.publishedTemplateCount))}</strong></div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="template-detail-summary mt-25px"><div><span>{pageCopy.summary.publicUrl}</span><strong>/{locale}/profile</strong></div><div><span>{pageCopy.summary.inviteCode}</span><strong>{inviteCode}</strong></div><div><span>{pageCopy.summary.booking}</span><strong>{profile.bookingUrl ? pageCopy.summary.configured : pageCopy.summary.notConfigured}</strong></div><div><span>{pageCopy.summary.templates}</span><strong>{pageCopy.summary.templateCount.replace('{{count}}', '8')}</strong></div></div>
|
|
|
|
|
<button type="button" className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 w-100 mt-15px" disabled={inviteCode.length !== 8} onClick={() => void navigator.clipboard.writeText(inviteCode)}>{pageCopy.copyCode}</button>
|
|
<button type="button" className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700 w-100 mt-15px" disabled={inviteCode.length !== 8} onClick={() => void navigator.clipboard.writeText(inviteCode)}>{pageCopy.copyCode}</button>
|
|
|
<Link to={path('/profile')} target="_blank" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 w-100 mt-25px">{pageCopy.viewOwn}</Link>
|
|
<Link to={path('/profile')} target="_blank" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700 w-100 mt-25px">{pageCopy.viewOwn}</Link>
|
|
|
</aside>
|
|
</aside>
|
|
|
|
|
|
|
|
<form id="stylist-profile-form" className="customer-edit-form workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px" onSubmit={handleSubmit}>
|
|
<form id="stylist-profile-form" className="customer-edit-form workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px" onSubmit={handleSubmit}>
|
|
|
- <span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.form.badge}</span><h2 className="fw-700 text-dark-gray fs-26 mb-25px mt-10px">{pageCopy.form.title}</h2>
|
|
|
|
|
|
|
+ <span className="fs-14 fw-700 text-base-color text-uppercase">{pageCopy.form.badge}</span>
|
|
|
|
|
+ <h2 className="fw-700 text-dark-gray fs-26 mb-25px mt-10px">{pageCopy.form.title}</h2>
|
|
|
{saved ? <div className="alert alert-success" role="status">{pageCopy.form.saved}</div> : null}
|
|
{saved ? <div className="alert alert-success" role="status">{pageCopy.form.saved}</div> : null}
|
|
|
- <div className="row g-3">
|
|
|
|
|
- <div className="col-12"><label htmlFor="profile-photo-upload" className="customer-edit-label">{pageCopy.form.fields.photo}</label><label htmlFor="profile-photo-upload" className="customer-photo-upload"><input id="profile-photo-upload" type="file" accept="image/*" onChange={(event) => handlePhoto(event.target.files?.[0])} /><span className="customer-photo-preview"><img src={profile.photo} alt={pageCopy.form.photoPreview} /></span><span>{pageCopy.form.changePhoto}</span></label></div>
|
|
|
|
|
- <ProfileInput id="profile-display-name" label={pageCopy.form.fields.displayName} value={profile.displayName} onChange={(displayName) => patch({ displayName })} />
|
|
|
|
|
- <ProfileInput id="profile-slug" label={pageCopy.form.fields.slug} value={profile.slug} onChange={(slug) => patch({ slug })} />
|
|
|
|
|
- <ProfileInput id="profile-years" label={pageCopy.form.fields.years} value={profile.years} col="col-md-4" onChange={(years) => patch({ years })} />
|
|
|
|
|
- <ProfileInput id="profile-rating" label={pageCopy.form.fields.rating} value={profile.rating} col="col-md-4" onChange={(rating) => patch({ rating })} />
|
|
|
|
|
- <ProfileInput id="profile-review-count" label={pageCopy.form.fields.reviewCount} value={profile.reviewCount} col="col-md-4" onChange={(reviewCount) => patch({ reviewCount })} />
|
|
|
|
|
- <ProfileInput id="profile-salon" label={pageCopy.form.fields.salonName} value={profile.salonName} col="col-12" onChange={(salonName) => patch({ salonName })} />
|
|
|
|
|
- <ProfileTextarea id="profile-lead" label={pageCopy.form.fields.lead} value={profile.lead} rows={3} onChange={(lead) => patch({ lead })} />
|
|
|
|
|
- <ProfileTextarea id="profile-bio" label={pageCopy.form.fields.bio} value={profile.bio} rows={5} onChange={(bio) => patch({ bio })} />
|
|
|
|
|
- <ProfileInput id="profile-booking-url" label={pageCopy.form.fields.bookingUrl} value={profile.bookingUrl} type="url" col="col-12" onChange={(bookingUrl) => patch({ bookingUrl })} />
|
|
|
|
|
- <ProfileTextarea id="profile-address" label={pageCopy.form.fields.address} value={profile.address} rows={3} onChange={(address) => patch({ address })} />
|
|
|
|
|
- <ProfileTextarea id="profile-hours" label={pageCopy.form.fields.hours} value={profile.hours} rows={4} col="col-md-6" onChange={(hours) => patch({ hours })} />
|
|
|
|
|
- <ProfileTextarea id="profile-specialties" label={pageCopy.form.fields.specialties} value={profile.specialties} rows={4} col="col-md-6" onChange={(specialties) => patch({ specialties })} />
|
|
|
|
|
|
|
+ {error ? <div className="alert alert-danger" role="alert">{error}</div> : null}
|
|
|
|
|
+
|
|
|
|
|
+ <section className="stylist-profile-form-section">
|
|
|
|
|
+ <h3>{sections.hero.title}</h3>
|
|
|
|
|
+ <p>{sections.hero.lead}</p>
|
|
|
|
|
+ <div className="row g-3">
|
|
|
|
|
+ <div className="col-12">
|
|
|
|
|
+ <label htmlFor="profile-photo-upload" className="customer-edit-label">{fieldCopy.photo}</label>
|
|
|
|
|
+ <label htmlFor="profile-photo-upload" className="customer-photo-upload">
|
|
|
|
|
+ <input id="profile-photo-upload" type="file" accept="image/*" onChange={(event) => { setPhotoFile(event.target.files?.[0] ?? null); setSaved(false) }} />
|
|
|
|
|
+ <span className="customer-photo-preview"><img src={photoPreview} alt={pageCopy.form.photoPreview} /></span>
|
|
|
|
|
+ <span>{pageCopy.form.changePhoto}</span>
|
|
|
|
|
+ </label>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <ProfileInput id="profile-display-name" label={fieldCopy.displayName} value={profile.displayName} onChange={(displayName) => patch({ displayName })} required />
|
|
|
|
|
+ <ProfileInput id="profile-slug" label={fieldCopy.slug} value={profile.slug} onChange={(slug) => patch({ slug: slug.toLowerCase() })} hint={pageCopy.form.slugHint.replace('{{locale}}', locale)} required />
|
|
|
|
|
+ <ProfileTextarea id="profile-lead" label={fieldCopy.lead} value={profile.lead} rows={3} onChange={(lead) => patch({ lead })} />
|
|
|
|
|
+ <ProfileTextarea id="profile-bio" label={fieldCopy.bio} value={profile.bio} rows={5} onChange={(bio) => patch({ bio })} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section className="stylist-profile-form-section">
|
|
|
|
|
+ <h3>{sections.booking.title}</h3>
|
|
|
|
|
+ <p>{sections.booking.lead}</p>
|
|
|
|
|
+ <div className="row g-3">
|
|
|
|
|
+ <ProfileInput id="profile-booking-url" label={fieldCopy.bookingUrl} value={profile.bookingUrl} type="url" col="col-12" onChange={(bookingUrl) => patch({ bookingUrl })} hint={pageCopy.form.bookingHint} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section className="stylist-profile-form-section">
|
|
|
|
|
+ <h3>{sections.stats.title}</h3>
|
|
|
|
|
+ <p>{sections.stats.lead}</p>
|
|
|
|
|
+ <div className="row g-3">
|
|
|
|
|
+ <div className="col-md-4">
|
|
|
|
|
+ <label htmlFor="profile-years" className="customer-edit-label">{fieldCopy.years}</label>
|
|
|
|
|
+ <select id="profile-years" className="form-control customer-edit-control" value={profile.experienceYears ?? ''} onChange={(event) => patch({ experienceYears: event.target.value ? Number(event.target.value) : null })}>
|
|
|
|
|
+ <option value="">{pageCopy.form.yearsPlaceholder}</option>
|
|
|
|
|
+ {EXPERIENCE_YEARS.map((year) => (
|
|
|
|
|
+ <option key={year} value={year}>{year}</option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </select>
|
|
|
|
|
+ <span className="stylist-profile-field-hint">{pageCopy.form.yearsHint.replace('{{unit}}', yearUnit)}</span>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <ProfileInput id="profile-rating" label={fieldCopy.rating} value={profile.rating} col="col-md-4" onChange={(rating) => patch({ rating })} />
|
|
|
|
|
+ <ProfileInput id="profile-review-count" label={fieldCopy.reviewCount} value={String(profile.reviewCount || '')} col="col-md-4" onChange={(reviewCount) => patch({ reviewCount: Number(reviewCount.replace(/\D/g, '')) || 0 })} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <section className="stylist-profile-form-section">
|
|
|
|
|
+ <h3>{sections.salon.title}</h3>
|
|
|
|
|
+ <p>{sections.salon.lead}</p>
|
|
|
|
|
+ <div className="row g-3">
|
|
|
|
|
+ <ProfileTextarea id="profile-specialties" label={fieldCopy.specialties} value={profile.specialties} rows={4} onChange={(specialties) => patch({ specialties })} />
|
|
|
|
|
+ <ProfileTextarea id="profile-address" label={fieldCopy.address} value={profile.address} rows={3} onChange={(address) => patch({ address })} />
|
|
|
|
|
+ <ProfileTextarea id="profile-hours" label={fieldCopy.hours} value={profile.hours} rows={4} onChange={(hours) => patch({ hours })} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </section>
|
|
|
|
|
+
|
|
|
|
|
+ <div className="customer-edit-actions border-radius-8px p-25px mt-4">
|
|
|
|
|
+ <div><strong>{pageCopy.form.submitTitle}</strong><span>{pageCopy.form.submitLead}</span></div>
|
|
|
|
|
+ <div className="d-flex flex-wrap gap-2">
|
|
|
|
|
+ <button type="button" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700" onClick={() => navigate(path('/dashboard'))}>{copy.common.cancel}</button>
|
|
|
|
|
+ <button type="submit" disabled={saving} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-save me-10px"></i>{saving ? copy.common.sending : copy.common.save}</button>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="customer-edit-actions border-radius-8px p-25px mt-4"><div><strong>{pageCopy.form.submitTitle}</strong><span>{pageCopy.form.submitLead}</span></div><div className="d-flex flex-wrap gap-2"><button type="button" className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700" onClick={() => navigate(path('/dashboard'))}>{copy.common.cancel}</button><button type="submit" className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-save me-10px"></i>{copy.common.save}</button></div></div>
|
|
|
|
|
</form>
|
|
</form>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -102,10 +231,41 @@ export function StylistProfileManagePage() {
|
|
|
)
|
|
)
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function ProfileInput({ id, label, value, onChange, type = 'text', col = 'col-md-6' }: { id: string; label: string; value: string; onChange: (value: string) => void; type?: string; col?: string }) {
|
|
|
|
|
- return <div className={col}><label htmlFor={id} className="customer-edit-label">{label}</label><input id={id} className="form-control customer-edit-control" type={type} value={value} onChange={(event) => onChange(event.target.value)} /></div>
|
|
|
|
|
|
|
+function ProfileInput({
|
|
|
|
|
+ id, label, value, onChange, type = 'text', col = 'col-md-6', required = false, hint,
|
|
|
|
|
+}: {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ label: string
|
|
|
|
|
+ value: string
|
|
|
|
|
+ onChange: (value: string) => void
|
|
|
|
|
+ type?: string
|
|
|
|
|
+ col?: string
|
|
|
|
|
+ required?: boolean
|
|
|
|
|
+ hint?: string
|
|
|
|
|
+}) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className={col}>
|
|
|
|
|
+ <label htmlFor={id} className="customer-edit-label">{label}</label>
|
|
|
|
|
+ <input id={id} className="form-control customer-edit-control" type={type} value={value} required={required} onChange={(event) => onChange(event.target.value)} />
|
|
|
|
|
+ {hint ? <span className="stylist-profile-field-hint">{hint}</span> : null}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-function ProfileTextarea({ id, label, value, onChange, rows, col = 'col-12' }: { id: string; label: string; value: string; onChange: (value: string) => void; rows: number; col?: string }) {
|
|
|
|
|
- return <div className={col}><label htmlFor={id} className="customer-edit-label">{label}</label><textarea id={id} className="form-control customer-edit-control" rows={rows} value={value} onChange={(event) => onChange(event.target.value)} /></div>
|
|
|
|
|
|
|
+function ProfileTextarea({
|
|
|
|
|
+ id, label, value, onChange, rows, col = 'col-12',
|
|
|
|
|
+}: {
|
|
|
|
|
+ id: string
|
|
|
|
|
+ label: string
|
|
|
|
|
+ value: string
|
|
|
|
|
+ onChange: (value: string) => void
|
|
|
|
|
+ rows: number
|
|
|
|
|
+ col?: string
|
|
|
|
|
+}) {
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div className={col}>
|
|
|
|
|
+ <label htmlFor={id} className="customer-edit-label">{label}</label>
|
|
|
|
|
+ <textarea id={id} className="form-control customer-edit-control" rows={rows} value={value} onChange={(event) => onChange(event.target.value)} />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )
|
|
|
}
|
|
}
|