|
@@ -1,66 +1,66 @@
|
|
|
import React, { useEffect, useState } from 'react'
|
|
import React, { useEffect, useState } from 'react'
|
|
|
-import { Link } from 'react-router-dom'
|
|
|
|
|
|
|
+import { Link, useSearchParams } from 'react-router-dom'
|
|
|
import { Helmet } from 'react-helmet-async'
|
|
import { Helmet } from 'react-helmet-async'
|
|
|
|
|
+import { DashboardLayout } from '../../../layouts/DashboardLayout'
|
|
|
import { listCustomers } from '../../../services/api/customers'
|
|
import { listCustomers } from '../../../services/api/customers'
|
|
|
-import { listAftercareOptions, listSimulationTemplateGallery } from '../../../services/api/pageData'
|
|
|
|
|
-import type { AftercareOptionEntity, SimulationTemplateEntity } from '../../../services/api/contracts'
|
|
|
|
|
-import { createSimulation, uploadMedia } from '../../../services/api/simulations'
|
|
|
|
|
import type { CustomerEntity } from '../../../services/api/customers'
|
|
import type { CustomerEntity } from '../../../services/api/customers'
|
|
|
-import { TEMPLATE_LIMIT, usePageContext, usePreviewUrl } from '../../../shared/pageContext'
|
|
|
|
|
-
|
|
|
|
|
-const TEMPLATE_PAGE_SIZE = 6
|
|
|
|
|
-const AFTERCARE_SIMULATION_TYPES = [
|
|
|
|
|
- 'home_styling',
|
|
|
|
|
- 'after_30_days',
|
|
|
|
|
- 'after_60_days',
|
|
|
|
|
-] as const
|
|
|
|
|
-import { simulationSession } from '../../../shared/simulationSession'
|
|
|
|
|
-import { DashboardLayout } from '../../../layouts/DashboardLayout'
|
|
|
|
|
|
|
+import type { SimulationTemplateOption } from '../../../services/api/contracts'
|
|
|
|
|
+import {
|
|
|
|
|
+ createSimulation,
|
|
|
|
|
+ listSimulationTemplates,
|
|
|
|
|
+ uploadMedia,
|
|
|
|
|
+} from '../../../services/api/simulations'
|
|
|
|
|
+import {
|
|
|
|
|
+ TEMPLATE_LIMIT,
|
|
|
|
|
+ usePageContext,
|
|
|
|
|
+ usePreviewUrl,
|
|
|
|
|
+} from '../../../shared/pageContext'
|
|
|
|
|
|
|
|
export function StylistSimulationPage() {
|
|
export function StylistSimulationPage() {
|
|
|
|
|
+ const [searchParams] = useSearchParams()
|
|
|
const { locale, navigate, path, copy } = usePageContext()
|
|
const { locale, navigate, path, copy } = usePageContext()
|
|
|
- const [customerMode, setCustomerMode] = useState('selected')
|
|
|
|
|
- const [simulationType, setSimulationType] = useState('hairstyle')
|
|
|
|
|
|
|
+ const presetCustomerId = searchParams.get('customerId') ?? ''
|
|
|
|
|
+ const pageCopy = copy.simulationCreatePage
|
|
|
|
|
+
|
|
|
|
|
+ const [bindCustomer, setBindCustomer] = useState(Boolean(presetCustomerId))
|
|
|
|
|
+ const [selectedCustomerId, setSelectedCustomerId] = useState(presetCustomerId)
|
|
|
|
|
+ const [templateSource, setTemplateSource] = useState<'STYLIST' | 'SYSTEM'>('STYLIST')
|
|
|
const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
|
|
const [selectedTemplates, setSelectedTemplates] = useState<string[]>([])
|
|
|
- const [file, setFile] = useState<File | null>(null)
|
|
|
|
|
const [customers, setCustomers] = useState<CustomerEntity[]>([])
|
|
const [customers, setCustomers] = useState<CustomerEntity[]>([])
|
|
|
- const [selectedCustomerId, setSelectedCustomerId] = useState<string | null>(null)
|
|
|
|
|
- const [templateGallery, setTemplateGallery] = useState<SimulationTemplateEntity[]>([])
|
|
|
|
|
- const [templatePage, setTemplatePage] = useState(1)
|
|
|
|
|
- const [templateTotal, setTemplateTotal] = useState(0)
|
|
|
|
|
- const [aftercareOptions, setAftercareOptions] = useState<AftercareOptionEntity[]>([])
|
|
|
|
|
|
|
+ const [templates, setTemplates] = useState<SimulationTemplateOption[]>([])
|
|
|
|
|
+ const [file, setFile] = useState<File | null>(null)
|
|
|
|
|
+ const [originImageUrl, setOriginImageUrl] = useState('')
|
|
|
|
|
+ const [remark, setRemark] = useState('')
|
|
|
|
|
+ const [uploading, setUploading] = useState(false)
|
|
|
const [submitting, setSubmitting] = useState(false)
|
|
const [submitting, setSubmitting] = useState(false)
|
|
|
const [error, setError] = useState('')
|
|
const [error, setError] = useState('')
|
|
|
- const previewUrl = usePreviewUrl(file, '/images/aitori-before-photo.png')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const previewUrl = usePreviewUrl(file, originImageUrl || '/images/aitori-before-photo.png')
|
|
|
const selectedCustomer = customers.find((item) => item.id === selectedCustomerId) || null
|
|
const selectedCustomer = customers.find((item) => item.id === selectedCustomerId) || null
|
|
|
- const pageCopy = copy.stylistSimulationPage
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- listCustomers().then((result) => {
|
|
|
|
|
|
|
+ void listCustomers().then((result) => {
|
|
|
setCustomers(result.items)
|
|
setCustomers(result.items)
|
|
|
- const preset = simulationSession.getCustomerId()
|
|
|
|
|
- if (preset && result.items.some((item) => item.id === preset)) {
|
|
|
|
|
- setSelectedCustomerId(preset)
|
|
|
|
|
- } else if (result.items.length > 0) {
|
|
|
|
|
- setSelectedCustomerId(result.items[0].id)
|
|
|
|
|
|
|
+ if (presetCustomerId && result.items.some((item) => item.id === presetCustomerId)) {
|
|
|
|
|
+ setBindCustomer(true)
|
|
|
|
|
+ setSelectedCustomerId(presetCustomerId)
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
- listAftercareOptions().then(setAftercareOptions)
|
|
|
|
|
- }, [])
|
|
|
|
|
|
|
+ }, [presetCustomerId])
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
- listSimulationTemplateGallery(templatePage, TEMPLATE_PAGE_SIZE)
|
|
|
|
|
- .then(({ items, total }) => {
|
|
|
|
|
- setTemplateGallery(items)
|
|
|
|
|
- setTemplateTotal(total)
|
|
|
|
|
|
|
+ void listSimulationTemplates({
|
|
|
|
|
+ source: templateSource,
|
|
|
|
|
+ page: 1,
|
|
|
|
|
+ pageSize: 12,
|
|
|
|
|
+ })
|
|
|
|
|
+ .then((result) => {
|
|
|
|
|
+ setTemplates(result.items)
|
|
|
})
|
|
})
|
|
|
.catch(() => {
|
|
.catch(() => {
|
|
|
- setTemplateGallery([])
|
|
|
|
|
- setTemplateTotal(0)
|
|
|
|
|
|
|
+ setTemplates([])
|
|
|
})
|
|
})
|
|
|
- }, [templatePage])
|
|
|
|
|
-
|
|
|
|
|
- const templateTotalPages = Math.max(1, Math.ceil(templateTotal / TEMPLATE_PAGE_SIZE))
|
|
|
|
|
|
|
+ }, [templateSource])
|
|
|
|
|
|
|
|
const toggleTemplate = (templateId: string) => {
|
|
const toggleTemplate = (templateId: string) => {
|
|
|
setSelectedTemplates((current) => {
|
|
setSelectedTemplates((current) => {
|
|
@@ -74,45 +74,67 @@ export function StylistSimulationPage() {
|
|
|
})
|
|
})
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const handleUpload = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
|
|
|
+ const nextFile = event.target.files?.[0] || null
|
|
|
|
|
+ setFile(nextFile)
|
|
|
|
|
+ setError('')
|
|
|
|
|
+
|
|
|
|
|
+ if (!nextFile) {
|
|
|
|
|
+ setOriginImageUrl('')
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setUploading(true)
|
|
|
|
|
+ try {
|
|
|
|
|
+ const uploaded = await uploadMedia(nextFile, 'ai-user-origin-image')
|
|
|
|
|
+ setOriginImageUrl(uploaded.url)
|
|
|
|
|
+ } catch (uploadError) {
|
|
|
|
|
+ setOriginImageUrl('')
|
|
|
|
|
+ setError(
|
|
|
|
|
+ uploadError instanceof Error
|
|
|
|
|
+ ? uploadError.message
|
|
|
|
|
+ : pageCopy.errors.submitFailed,
|
|
|
|
|
+ )
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setUploading(false)
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
|
|
|
event.preventDefault()
|
|
event.preventDefault()
|
|
|
if (submitting) return
|
|
if (submitting) return
|
|
|
- if (customerMode === 'selected' && !selectedCustomer) {
|
|
|
|
|
- setError(pageCopy.step1.emptyLead)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (bindCustomer && !selectedCustomerId) {
|
|
|
|
|
+ setError(pageCopy.errors.customerRequired)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (!file) {
|
|
|
|
|
- setError(pageCopy.step2.requiredLead || '请先上传顾客照片')
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (!originImageUrl) {
|
|
|
|
|
+ setError(pageCopy.errors.originRequired)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
- if (simulationType === 'hairstyle' && selectedTemplates.length === 0) {
|
|
|
|
|
- setError(pageCopy.step3.templateLead)
|
|
|
|
|
|
|
+
|
|
|
|
|
+ if (selectedTemplates.length === 0) {
|
|
|
|
|
+ setError(pageCopy.errors.templateRequired)
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
setError('')
|
|
setError('')
|
|
|
setSubmitting(true)
|
|
setSubmitting(true)
|
|
|
try {
|
|
try {
|
|
|
- const sourcePhoto = (await uploadMedia(file)).url
|
|
|
|
|
-
|
|
|
|
|
- const useSelected = customerMode === 'selected' && !!selectedCustomer
|
|
|
|
|
- const job = await createSimulation({
|
|
|
|
|
- customerMode: useSelected ? 'selected' : 'guest',
|
|
|
|
|
- customerId: useSelected ? selectedCustomer!.id : undefined,
|
|
|
|
|
- sourcePhoto,
|
|
|
|
|
- simulationType: simulationType === 'aftercare' ? 'aftercare' : 'hairstyle',
|
|
|
|
|
- templateIds: simulationType === 'hairstyle' ? selectedTemplates : undefined,
|
|
|
|
|
- aftercareTypes:
|
|
|
|
|
- simulationType === 'aftercare'
|
|
|
|
|
- ? [...AFTERCARE_SIMULATION_TYPES]
|
|
|
|
|
- : undefined,
|
|
|
|
|
- })
|
|
|
|
|
- simulationSession.setJob(job.id, {
|
|
|
|
|
- customerId: useSelected ? selectedCustomer!.id : undefined,
|
|
|
|
|
- sourcePhoto,
|
|
|
|
|
|
|
+ const response = await createSimulation({
|
|
|
|
|
+ customerId: bindCustomer && selectedCustomerId ? selectedCustomerId : undefined,
|
|
|
|
|
+ originImage: originImageUrl,
|
|
|
|
|
+ templateIds: selectedTemplates,
|
|
|
|
|
+ simulateRemark: remark,
|
|
|
})
|
|
})
|
|
|
- navigate(path(simulationType === 'aftercare' ? '/simulation/aftercare' : '/simulation/result'))
|
|
|
|
|
- } catch (err) {
|
|
|
|
|
- setError(err instanceof Error ? err.message : '发起模拟失败')
|
|
|
|
|
|
|
+ navigate(path(`/simulation/result/${response.simulationId}`))
|
|
|
|
|
+ } catch (submitError) {
|
|
|
|
|
+ setError(
|
|
|
|
|
+ submitError instanceof Error
|
|
|
|
|
+ ? submitError.message
|
|
|
|
|
+ : pageCopy.errors.submitFailed,
|
|
|
|
|
+ )
|
|
|
} finally {
|
|
} finally {
|
|
|
setSubmitting(false)
|
|
setSubmitting(false)
|
|
|
}
|
|
}
|
|
@@ -128,11 +150,27 @@ export function StylistSimulationPage() {
|
|
|
<section className="stylist-workbench-top ipad-top-space-margin mb-2">
|
|
<section className="stylist-workbench-top ipad-top-space-margin mb-2">
|
|
|
<div className="container">
|
|
<div className="container">
|
|
|
<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>
|
|
|
|
|
- <button type="submit" form="simulation-form" disabled={submitting} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-cpu me-10px"></i>{submitting ? '...' : pageCopy.submit}</button>
|
|
|
|
|
|
|
+ <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>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ form="simulation-form"
|
|
|
|
|
+ disabled={submitting || uploading}
|
|
|
|
|
+ className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i className="feather icon-feather-cpu me-10px"></i>
|
|
|
|
|
+ {submitting ? '...' : pageCopy.submit}
|
|
|
|
|
+ </button>
|
|
|
</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">
|
|
|
- <span className="bg-base-color d-inline-block mb-15px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">{pageCopy.badge}</span>
|
|
|
|
|
|
|
+ <span className="bg-base-color d-inline-block mb-15px fw-600 text-white border-radius-30px ps-20px pe-20px fs-12">
|
|
|
|
|
+ {pageCopy.badge}
|
|
|
|
|
+ </span>
|
|
|
<h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{pageCopy.title}</h1>
|
|
<h1 className="fw-700 text-dark-gray ls-minus-1px mb-10px">{pageCopy.title}</h1>
|
|
|
<p className="fs-18 lh-30 mb-0">{pageCopy.lead}</p>
|
|
<p className="fs-18 lh-30 mb-0">{pageCopy.lead}</p>
|
|
|
</div>
|
|
</div>
|
|
@@ -145,50 +183,68 @@ export function StylistSimulationPage() {
|
|
|
<div className="simulation-step-heading">
|
|
<div className="simulation-step-heading">
|
|
|
<span>1</span>
|
|
<span>1</span>
|
|
|
<div>
|
|
<div>
|
|
|
- <span className="fs-14 fw-700 text-base-color">{pageCopy.step1.badge}</span>
|
|
|
|
|
- <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.step1.title}</h2>
|
|
|
|
|
|
|
+ <span className="fs-14 fw-700 text-base-color">{pageCopy.bindCustomer}</span>
|
|
|
|
|
+ <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.bindCustomer}</h2>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="simulation-choice-grid mt-25px">
|
|
<div className="simulation-choice-grid mt-25px">
|
|
|
<label className="simulation-choice-card">
|
|
<label className="simulation-choice-card">
|
|
|
- <input type="radio" name="customer-mode" value="selected" checked={customerMode === 'selected'} onChange={() => setCustomerMode('selected')} />
|
|
|
|
|
- <span className="simulation-choice-icon"><i className="feather icon-feather-user-check"></i></span>
|
|
|
|
|
- <strong>{pageCopy.step1.selectedTitle}</strong>
|
|
|
|
|
- <small>{pageCopy.step1.selectedLead}</small>
|
|
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="customer-mode"
|
|
|
|
|
+ aria-label={pageCopy.bindCustomer}
|
|
|
|
|
+ checked={bindCustomer}
|
|
|
|
|
+ onChange={() => setBindCustomer(true)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="simulation-choice-icon">
|
|
|
|
|
+ <i className="feather icon-feather-user-check"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <strong>{pageCopy.bindCustomer}</strong>
|
|
|
|
|
+ <small>{pageCopy.customerPlaceholder}</small>
|
|
|
</label>
|
|
</label>
|
|
|
<label className="simulation-choice-card">
|
|
<label className="simulation-choice-card">
|
|
|
- <input type="radio" name="customer-mode" value="guest" checked={customerMode === 'guest'} onChange={() => setCustomerMode('guest')} />
|
|
|
|
|
- <span className="simulation-choice-icon"><i className="feather icon-feather-user-x"></i></span>
|
|
|
|
|
- <strong>{pageCopy.step1.guestTitle}</strong>
|
|
|
|
|
- <small>{pageCopy.step1.guestLead}</small>
|
|
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="customer-mode"
|
|
|
|
|
+ aria-label={pageCopy.noCustomer}
|
|
|
|
|
+ checked={!bindCustomer}
|
|
|
|
|
+ onChange={() => setBindCustomer(false)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="simulation-choice-icon">
|
|
|
|
|
+ <i className="feather icon-feather-user-x"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <strong>{pageCopy.noCustomer}</strong>
|
|
|
|
|
+ <small>{pageCopy.noCustomer}</small>
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="simulation-selected-customer mt-25px" hidden={customerMode !== 'selected'}>
|
|
|
|
|
- <img src={selectedCustomer?.image || '/images/aitori-before-photo.png'} alt={selectedCustomer?.name || pageCopy.step1.currentCustomer} />
|
|
|
|
|
- <div>
|
|
|
|
|
- <span className="fs-14 text-medium-gray d-block">{pageCopy.step1.currentCustomer}</span>
|
|
|
|
|
- {customers.length > 0 ? (
|
|
|
|
|
- <select
|
|
|
|
|
- className="form-select customer-edit-control mt-10px"
|
|
|
|
|
- value={selectedCustomerId || ''}
|
|
|
|
|
- onChange={(event) => setSelectedCustomerId(event.target.value)}
|
|
|
|
|
- >
|
|
|
|
|
- {customers.map((item) => (
|
|
|
|
|
- <option key={item.id} value={item.id}>{item.name}</option>
|
|
|
|
|
- ))}
|
|
|
|
|
- </select>
|
|
|
|
|
- ) : (
|
|
|
|
|
- <strong className="text-dark-gray">{pageCopy.step1.emptyCustomer}</strong>
|
|
|
|
|
- )}
|
|
|
|
|
- <span className="d-block fs-14 text-medium-gray mt-10px">
|
|
|
|
|
- {selectedCustomer ? pageCopy.step1.selectedSummary.replace('{{date}}', selectedCustomer.lastVisit || '—') : pageCopy.step1.emptyLead}
|
|
|
|
|
- </span>
|
|
|
|
|
|
|
+ <div className="simulation-selected-customer mt-25px">
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={selectedCustomer?.avatar || '/images/aitori-before-photo.png'}
|
|
|
|
|
+ alt={selectedCustomer?.name || pageCopy.customerLabel}
|
|
|
|
|
+ />
|
|
|
|
|
+ <div className="w-100">
|
|
|
|
|
+ <label
|
|
|
|
|
+ htmlFor="simulation-customer-select"
|
|
|
|
|
+ className="fs-14 text-medium-gray d-block"
|
|
|
|
|
+ >
|
|
|
|
|
+ {pageCopy.customerLabel}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <select
|
|
|
|
|
+ id="simulation-customer-select"
|
|
|
|
|
+ aria-label={pageCopy.customerLabel}
|
|
|
|
|
+ className="form-select customer-edit-control mt-10px"
|
|
|
|
|
+ value={selectedCustomerId}
|
|
|
|
|
+ disabled={!bindCustomer}
|
|
|
|
|
+ onChange={(event) => setSelectedCustomerId(event.target.value)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <option value="">{pageCopy.customerPlaceholder}</option>
|
|
|
|
|
+ {customers.map((item) => (
|
|
|
|
|
+ <option key={item.id} value={item.id}>
|
|
|
|
|
+ {item.name}
|
|
|
|
|
+ </option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </select>
|
|
|
</div>
|
|
</div>
|
|
|
- <Link to={path('/customers')} className="btn btn-small btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{pageCopy.step1.changeCustomer}</Link>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="simulation-save-warning mt-25px" hidden={customerMode !== 'guest'}>
|
|
|
|
|
- <i className="feather icon-feather-alert-circle"></i>
|
|
|
|
|
- <span>{pageCopy.step1.warning}</span>
|
|
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -196,14 +252,24 @@ export function StylistSimulationPage() {
|
|
|
<div className="simulation-step-heading">
|
|
<div className="simulation-step-heading">
|
|
|
<span>2</span>
|
|
<span>2</span>
|
|
|
<div>
|
|
<div>
|
|
|
- <span className="fs-14 fw-700 text-base-color">{pageCopy.step2.badge}</span>
|
|
|
|
|
- <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.step2.title}</h2>
|
|
|
|
|
|
|
+ <span className="fs-14 fw-700 text-base-color">{pageCopy.upload}</span>
|
|
|
|
|
+ <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.upload}</h2>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<label htmlFor="simulation-photo-upload" className="simulation-photo-upload mt-25px">
|
|
<label htmlFor="simulation-photo-upload" className="simulation-photo-upload mt-25px">
|
|
|
- <input id="simulation-photo-upload" type="file" accept="image/*" onChange={(event) => setFile(event.target.files?.[0] || null)} />
|
|
|
|
|
- <img id="simulation-photo-preview" src={previewUrl} alt={pageCopy.step2.previewAlt} />
|
|
|
|
|
- <span><i className="feather icon-feather-upload me-8px"></i>{pageCopy.step2.upload}</span>
|
|
|
|
|
|
|
+ <input
|
|
|
|
|
+ id="simulation-photo-upload"
|
|
|
|
|
+ aria-label={pageCopy.upload}
|
|
|
|
|
+ type="file"
|
|
|
|
|
+ accept="image/*"
|
|
|
|
|
+ onChange={handleUpload}
|
|
|
|
|
+ />
|
|
|
|
|
+ <img id="simulation-photo-preview" src={previewUrl} alt={pageCopy.upload} />
|
|
|
|
|
+ <span>
|
|
|
|
|
+ <i className="feather icon-feather-upload me-8px"></i>
|
|
|
|
|
+ {uploading ? '...' : pageCopy.upload}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <small className="d-block mt-10px text-medium-gray">{pageCopy.uploadHint}</small>
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -211,101 +277,113 @@ export function StylistSimulationPage() {
|
|
|
<div className="simulation-step-heading">
|
|
<div className="simulation-step-heading">
|
|
|
<span>3</span>
|
|
<span>3</span>
|
|
|
<div>
|
|
<div>
|
|
|
- <span className="fs-14 fw-700 text-base-color">{pageCopy.step3.badge}</span>
|
|
|
|
|
- <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.step3.title}</h2>
|
|
|
|
|
|
|
+ <span className="fs-14 fw-700 text-base-color">{pageCopy.myTemplates}</span>
|
|
|
|
|
+ <h2 className="fw-700 text-dark-gray fs-26 mb-0">{pageCopy.myTemplates}</h2>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="simulation-choice-grid mt-25px mb-30px">
|
|
<div className="simulation-choice-grid mt-25px mb-30px">
|
|
|
<label className="simulation-choice-card">
|
|
<label className="simulation-choice-card">
|
|
|
- <input type="radio" name="simulation-type" value="hairstyle" checked={simulationType === 'hairstyle'} onChange={() => setSimulationType('hairstyle')} />
|
|
|
|
|
- <span className="simulation-choice-icon"><i className="feather icon-feather-scissors"></i></span>
|
|
|
|
|
- <strong>{pageCopy.step3.hairstyleTitle}</strong>
|
|
|
|
|
- <small>{pageCopy.step3.hairstyleLead}</small>
|
|
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="template-source"
|
|
|
|
|
+ aria-label={pageCopy.myTemplates}
|
|
|
|
|
+ checked={templateSource === 'STYLIST'}
|
|
|
|
|
+ onChange={() => setTemplateSource('STYLIST')}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="simulation-choice-icon">
|
|
|
|
|
+ <i className="feather icon-feather-scissors"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <strong>{pageCopy.myTemplates}</strong>
|
|
|
|
|
+ <small>{pageCopy.myTemplates}</small>
|
|
|
</label>
|
|
</label>
|
|
|
<label className="simulation-choice-card">
|
|
<label className="simulation-choice-card">
|
|
|
- <input type="radio" name="simulation-type" value="aftercare" checked={simulationType === 'aftercare'} onChange={() => setSimulationType('aftercare')} />
|
|
|
|
|
- <span className="simulation-choice-icon"><i className="feather icon-feather-calendar"></i></span>
|
|
|
|
|
- <strong>{pageCopy.step3.aftercareTitle}</strong>
|
|
|
|
|
- <small>{pageCopy.step3.aftercareLead}</small>
|
|
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="radio"
|
|
|
|
|
+ name="template-source"
|
|
|
|
|
+ aria-label={pageCopy.systemTemplates}
|
|
|
|
|
+ checked={templateSource === 'SYSTEM'}
|
|
|
|
|
+ onChange={() => setTemplateSource('SYSTEM')}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span className="simulation-choice-icon">
|
|
|
|
|
+ <i className="feather icon-feather-layers"></i>
|
|
|
|
|
+ </span>
|
|
|
|
|
+ <strong>{pageCopy.systemTemplates}</strong>
|
|
|
|
|
+ <small>{pageCopy.systemTemplates}</small>
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
- <div data-simulation-panel="hairstyle" hidden={simulationType !== 'hairstyle'}>
|
|
|
|
|
- <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
|
|
|
|
|
- <p className="mb-0 fs-15 text-medium-gray">{pageCopy.step3.templateLead}</p>
|
|
|
|
|
- <span className="simulation-count-badge">{pageCopy.step3.selectedCount} <strong>{selectedTemplates.length}</strong> / 5</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="simulation-template-grid">
|
|
|
|
|
- {templateGallery.map((template) => {
|
|
|
|
|
|
|
+ <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
|
|
|
|
|
+ <p className="mb-0 fs-15 text-medium-gray">
|
|
|
|
|
+ {pageCopy.selectedCount.replace('{{count}}', String(selectedTemplates.length))}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div className="simulation-template-grid">
|
|
|
|
|
+ {templates.length === 0 ? (
|
|
|
|
|
+ <p className="text-medium-gray mb-0">{pageCopy.emptyTemplates}</p>
|
|
|
|
|
+ ) : (
|
|
|
|
|
+ templates.map((template) => {
|
|
|
const active = selectedTemplates.includes(template.id)
|
|
const active = selectedTemplates.includes(template.id)
|
|
|
|
|
+
|
|
|
return (
|
|
return (
|
|
|
<label key={template.id} className="simulation-template-card">
|
|
<label key={template.id} className="simulation-template-card">
|
|
|
- <input type="checkbox" name="templates" value={template.id} checked={active} disabled={!active && selectedTemplates.length >= TEMPLATE_LIMIT} onChange={() => toggleTemplate(template.id)} />
|
|
|
|
|
- <img src={template.image} alt={template.name} />
|
|
|
|
|
- <span>{template.name}</span>
|
|
|
|
|
|
|
+ <input
|
|
|
|
|
+ type="checkbox"
|
|
|
|
|
+ aria-label={template.templateName}
|
|
|
|
|
+ checked={active}
|
|
|
|
|
+ disabled={!active && selectedTemplates.length >= TEMPLATE_LIMIT}
|
|
|
|
|
+ onChange={() => toggleTemplate(template.id)}
|
|
|
|
|
+ />
|
|
|
|
|
+ <img
|
|
|
|
|
+ src={template.coverUrl || '/images/aitori-simulation-result.png'}
|
|
|
|
|
+ alt={template.templateName}
|
|
|
|
|
+ />
|
|
|
|
|
+ <span>{template.templateName}</span>
|
|
|
</label>
|
|
</label>
|
|
|
)
|
|
)
|
|
|
- })}
|
|
|
|
|
- </div>
|
|
|
|
|
- {templateTotalPages > 1 ? (
|
|
|
|
|
- <div className="customer-pager">
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- className={`customer-pager-link${templatePage <= 1 ? ' is-disabled' : ''}`}
|
|
|
|
|
- aria-label={pageCopy.step3.pagerPrev}
|
|
|
|
|
- disabled={templatePage <= 1}
|
|
|
|
|
- onClick={() => setTemplatePage((current) => Math.max(1, current - 1))}
|
|
|
|
|
- >
|
|
|
|
|
- {pageCopy.step3.pagerPrev}
|
|
|
|
|
- </button>
|
|
|
|
|
- {Array.from({ length: templateTotalPages }, (_, index) => index + 1).map((page) => (
|
|
|
|
|
- <button
|
|
|
|
|
- key={page}
|
|
|
|
|
- type="button"
|
|
|
|
|
- className={`customer-pager-link${page === templatePage ? ' is-active' : ''}`}
|
|
|
|
|
- aria-current={page === templatePage ? 'page' : undefined}
|
|
|
|
|
- onClick={() => setTemplatePage(page)}
|
|
|
|
|
- >
|
|
|
|
|
- {page}
|
|
|
|
|
- </button>
|
|
|
|
|
- ))}
|
|
|
|
|
- <button
|
|
|
|
|
- type="button"
|
|
|
|
|
- className={`customer-pager-link${templatePage >= templateTotalPages ? ' is-disabled' : ''}`}
|
|
|
|
|
- aria-label={pageCopy.step3.pagerNext}
|
|
|
|
|
- disabled={templatePage >= templateTotalPages}
|
|
|
|
|
- onClick={() => setTemplatePage((current) => Math.min(templateTotalPages, current + 1))}
|
|
|
|
|
- >
|
|
|
|
|
- {pageCopy.step3.pagerNext}
|
|
|
|
|
- </button>
|
|
|
|
|
- </div>
|
|
|
|
|
- ) : null}
|
|
|
|
|
|
|
+ })
|
|
|
|
|
+ )}
|
|
|
</div>
|
|
</div>
|
|
|
- <div data-simulation-panel="aftercare" hidden={simulationType !== 'aftercare'}>
|
|
|
|
|
- <div className="d-flex align-items-center justify-content-between flex-wrap gap-2 mb-25px">
|
|
|
|
|
- <p className="mb-0 fs-15 text-medium-gray">{pageCopy.step3.aftercarePanelLead}</p>
|
|
|
|
|
- <span className="simulation-count-badge">{pageCopy.step3.aftercareCount}</span>
|
|
|
|
|
- </div>
|
|
|
|
|
- <div className="simulation-template-grid simulation-aftercare-grid">
|
|
|
|
|
- {aftercareOptions.map((item) => (
|
|
|
|
|
- <label key={item.id} className="simulation-template-card">
|
|
|
|
|
- <input type="checkbox" name="aftercare-templates" value={item.id} checked readOnly />
|
|
|
|
|
- <img src={item.image} alt={item.title} />
|
|
|
|
|
- <span>{pageCopy.step3.aftercareItems[item.id as keyof typeof pageCopy.step3.aftercareItems] || item.title}</span>
|
|
|
|
|
- </label>
|
|
|
|
|
- ))}
|
|
|
|
|
- </div>
|
|
|
|
|
|
|
+ <div className="mt-25px">
|
|
|
|
|
+ <label
|
|
|
|
|
+ htmlFor="simulation-remark"
|
|
|
|
|
+ className="fs-14 fw-700 text-dark-gray d-block mb-10px"
|
|
|
|
|
+ >
|
|
|
|
|
+ {pageCopy.remark}
|
|
|
|
|
+ </label>
|
|
|
|
|
+ <textarea
|
|
|
|
|
+ id="simulation-remark"
|
|
|
|
|
+ className="form-control"
|
|
|
|
|
+ value={remark}
|
|
|
|
|
+ placeholder={pageCopy.remarkPlaceholder}
|
|
|
|
|
+ onChange={(event) => setRemark(event.target.value)}
|
|
|
|
|
+ />
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div className="customer-edit-actions workbench-panel border-radius-8px p-25px mt-4">
|
|
<div className="customer-edit-actions workbench-panel border-radius-8px p-25px mt-4">
|
|
|
<div>
|
|
<div>
|
|
|
- <strong>{pageCopy.submitPanel.title}</strong>
|
|
|
|
|
- <span>{pageCopy.submitPanel.lead}</span>
|
|
|
|
|
|
|
+ <strong>{pageCopy.submit}</strong>
|
|
|
|
|
+ <span>{pageCopy.lead}</span>
|
|
|
</div>
|
|
</div>
|
|
|
<div className="d-flex flex-wrap gap-2 align-items-center">
|
|
<div className="d-flex flex-wrap gap-2 align-items-center">
|
|
|
- {error ? <span className="fs-14 text-red me-2" role="alert">{error}</span> : null}
|
|
|
|
|
- <Link to={path('/dashboard')} className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{copy.common.cancel}</Link>
|
|
|
|
|
- <button type="submit" disabled={submitting} className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-cpu me-10px"></i>{submitting ? '...' : pageCopy.submit}</button>
|
|
|
|
|
|
|
+ {error ? (
|
|
|
|
|
+ <span className="fs-14 text-red me-2" role="alert">
|
|
|
|
|
+ {error}
|
|
|
|
|
+ </span>
|
|
|
|
|
+ ) : null}
|
|
|
|
|
+ <Link
|
|
|
|
|
+ to={path('/dashboard')}
|
|
|
|
|
+ className="btn btn-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"
|
|
|
|
|
+ >
|
|
|
|
|
+ {copy.common.cancel}
|
|
|
|
|
+ </Link>
|
|
|
|
|
+ <button
|
|
|
|
|
+ type="submit"
|
|
|
|
|
+ disabled={submitting || uploading}
|
|
|
|
|
+ className="btn btn-large btn-rounded btn-base-color btn-box-shadow fw-700"
|
|
|
|
|
+ >
|
|
|
|
|
+ <i className="feather icon-feather-cpu me-10px"></i>
|
|
|
|
|
+ {submitting ? '...' : pageCopy.submit}
|
|
|
|
|
+ </button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
</form>
|
|
</form>
|