| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- import React, { useEffect, useState } from 'react'
- import { Link, useSearchParams } from 'react-router-dom'
- import { Helmet } from 'react-helmet-async'
- import { DashboardLayout } from '../../../layouts/DashboardLayout'
- import { listCustomers } from '../../../services/api/customers'
- import type { CustomerEntity } from '../../../services/api/customers'
- import type { SimulationTemplateOption } from '../../../services/api/contracts'
- import {
- createSimulation,
- listSimulationTemplates,
- uploadMedia,
- } from '../../../services/api/simulations'
- import {
- TEMPLATE_LIMIT,
- usePageContext,
- usePreviewUrl,
- } from '../../../shared/pageContext'
- import { DEFAULT_USER_AVATAR_URL } from '../../../shared/uiAssets'
- export function StylistSimulationPage() {
- const [searchParams] = useSearchParams()
- const { locale, navigate, path, copy } = usePageContext()
- 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 [customers, setCustomers] = useState<CustomerEntity[]>([])
- 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 [error, setError] = useState('')
- const previewUrl = usePreviewUrl(file, originImageUrl || DEFAULT_USER_AVATAR_URL)
- const selectedCustomer = customers.find((item) => item.id === selectedCustomerId) || null
- useEffect(() => {
- void listCustomers().then((result) => {
- setCustomers(result.items)
- if (presetCustomerId && result.items.some((item) => item.id === presetCustomerId)) {
- setBindCustomer(true)
- setSelectedCustomerId(presetCustomerId)
- }
- })
- }, [presetCustomerId])
- useEffect(() => {
- void listSimulationTemplates({
- source: templateSource,
- page: 1,
- pageSize: 12,
- })
- .then((result) => {
- setTemplates(result.items)
- })
- .catch(() => {
- setTemplates([])
- })
- }, [templateSource])
- const toggleTemplate = (templateId: string) => {
- setSelectedTemplates((current) => {
- if (current.includes(templateId)) {
- return current.filter((item) => item !== templateId)
- }
- if (current.length >= TEMPLATE_LIMIT) {
- return current
- }
- return [...current, templateId]
- })
- }
- 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>) => {
- event.preventDefault()
- if (submitting) return
- if (bindCustomer && !selectedCustomerId) {
- setError(pageCopy.errors.customerRequired)
- return
- }
- if (!originImageUrl) {
- setError(pageCopy.errors.originRequired)
- return
- }
- if (selectedTemplates.length === 0) {
- setError(pageCopy.errors.templateRequired)
- return
- }
- setError('')
- setSubmitting(true)
- try {
- const response = await createSimulation({
- customerId: bindCustomer && selectedCustomerId ? selectedCustomerId : undefined,
- originImage: originImageUrl,
- templateIds: selectedTemplates,
- simulateRemark: remark,
- })
- navigate(path(`/simulation/result/${response.simulationId}`))
- } catch (submitError) {
- setError(
- submitError instanceof Error
- ? submitError.message
- : pageCopy.errors.submitFailed,
- )
- } finally {
- setSubmitting(false)
- }
- }
- return (
- <DashboardLayout>
- <Helmet>
- <html lang={locale === 'ja' ? 'ja' : 'zh-CN'} />
- <title>{`Aitori Stylist | ${pageCopy.title}`}</title>
- <meta name="description" content={pageCopy.description} />
- </Helmet>
- <section className="stylist-workbench-top ipad-top-space-margin mb-2">
- <div className="container">
- <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 || 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 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>
- <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>
- </div>
- </div>
- </section>
- <section className="pt-0">
- <div className="container">
- <form id="simulation-form" onSubmit={handleSubmit}>
- <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
- <div className="simulation-step-heading">
- <span>1</span>
- <div>
- <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 className="simulation-choice-grid mt-25px">
- <label className="simulation-choice-card">
- <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 className="simulation-choice-card">
- <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>
- </div>
- <div className="simulation-selected-customer mt-25px">
- <img
- src={selectedCustomer?.avatar || DEFAULT_USER_AVATAR_URL}
- 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>
- </div>
- <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px mb-4">
- <div className="simulation-step-heading">
- <span>2</span>
- <div>
- <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>
- <label htmlFor="simulation-photo-upload" className="simulation-photo-upload mt-25px">
- <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>
- </div>
- <div className="simulation-step workbench-panel bg-white border-radius-8px box-shadow-extra-large p-30px">
- <div className="simulation-step-heading">
- <span>3</span>
- <div>
- <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 className="simulation-choice-grid mt-25px mb-30px">
- <label className="simulation-choice-card">
- <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 className="simulation-choice-card">
- <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>
- </div>
- <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)
- return (
- <label key={template.id} className="simulation-template-card">
- <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>
- )
- })
- )}
- </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 className="customer-edit-actions workbench-panel border-radius-8px p-25px mt-4">
- <div>
- <strong>{pageCopy.submit}</strong>
- <span>{pageCopy.lead}</span>
- </div>
- <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 || 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>
- </form>
- </div>
- </section>
- </DashboardLayout>
- )
- }
|