Parcourir la source

feat: add stylist profile edit entries from dashboard nav and live preview

Logged-in stylists can open the public profile editor from the workbench header and from their live homepage, while customers still see the public page without an edit action.

Co-authored-by: Cursor <cursoragent@cursor.com>
zayen il y a 1 mois
Parent
commit
904e569a0a

+ 2 - 0
frontend/src/i18n/locales/en/translation.json

@@ -13,6 +13,7 @@
     "home": "Home",
     "customers": "Customers",
     "templates": "Hair templates",
+    "profile": "Profile",
     "logout": "Sign out"
   },
   "common": {
@@ -1395,6 +1396,7 @@
     "book": "Book a consultation",
     "tryAi": "Try with my photo",
     "browse": "Browse templates first",
+    "edit": "Edit profile",
     "yearUnit": " yrs",
     "untitled": "Stylist",
     "stats": { "experience": "Experience", "rating": "Rating", "reviews": "Reviews" },

+ 2 - 0
frontend/src/i18n/locales/ja/translation.json

@@ -13,6 +13,7 @@
     "home": "ホーム",
     "customers": "顧客",
     "templates": "テンプレート",
+    "profile": "プロフィール",
     "logout": "ログアウト"
   },
   "common": {
@@ -1055,6 +1056,7 @@
     "book": "予約する",
     "tryAi": "自分の写真で試す",
     "browse": "先にテンプレートを見る",
+    "edit": "プロフィールを編集",
     "yearUnit": "年",
     "untitled": "スタイリスト",
     "stats": { "experience": "経験", "rating": "評価", "reviews": "レビュー" },

+ 2 - 0
frontend/src/i18n/locales/zh/translation.json

@@ -13,6 +13,7 @@
     "home": "首页",
     "customers": "顾客",
     "templates": "发型模板",
+    "profile": "个人主页",
     "logout": "退出登录"
   },
   "common": {
@@ -1054,6 +1055,7 @@
     "book": "预约咨询",
     "tryAi": "用我的照片试发型",
     "browse": "先看发型模板",
+    "edit": "编辑个人主页",
     "yearUnit": "年",
     "untitled": "发型师",
     "stats": { "experience": "经验", "rating": "评分", "reviews": "评价" },

+ 1 - 0
frontend/src/layouts/DashboardLayout.tsx

@@ -46,6 +46,7 @@ function DashboardHeader() {
                 <li className="nav-item"><NavLink to={path('/dashboard')} className="nav-link">{navCopy.home}</NavLink></li>
                 <li className="nav-item"><NavLink to={path('/customers')} className="nav-link">{navCopy.customers}</NavLink></li>
                 <li className="nav-item"><NavLink to={path('/templates')} className="nav-link">{navCopy.templates}</NavLink></li>
+                <li className="nav-item"><NavLink to={path('/profile/manage')} className="nav-link">{navCopy.profile}</NavLink></li>
               </ul>
             </div>
           </div>

+ 2 - 0
frontend/src/pages/customer/StylistProfileLivePage.tsx

@@ -13,6 +13,7 @@ import {
   type StylistPublicTemplate,
 } from '../../services/api/stylist'
 import { ApiError } from '../../services/http/client'
+import { isAuthenticated } from '../../services/http/session'
 import { CUSTOMER_SIMULATION_CODE_KEY } from './CustomerSimulationPage'
 
 type LiveProfile = Omit<PublicStylistProfile, 'templates'> & {
@@ -119,6 +120,7 @@ export function StylistProfileLivePage({
               {profile.bookingUrl ? <a href={profile.bookingUrl} target="_blank" rel="noreferrer" className="btn btn-extra-large btn-rounded btn-dark-gray btn-box-shadow fw-700"><i className="feather icon-feather-calendar me-10px"></i>{pageCopy.book}</a> : null}
               <Link to={simulationPath} className="btn btn-extra-large btn-rounded btn-base-color btn-box-shadow fw-700"><i className="feather icon-feather-camera me-10px"></i>{pageCopy.tryAi}</Link>
               <a href="#templates" className="btn btn-extra-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700">{pageCopy.browse}</a>
+              {isAuthenticated() ? <Link to={path('/profile/manage')} className="btn btn-extra-large btn-rounded btn-transparent-light-gray border border-1 border-color-dark-gray fw-700"><i className="feather icon-feather-edit me-10px"></i>{pageCopy.edit}</Link> : null}
             </div>
             <div className="row row-cols-3 g-3 text-center public-profile-stats">
               <Stat value={experience} label={pageCopy.stats.experience} />

+ 14 - 0
frontend/src/pages/customer/__tests__/StylistProfilePage.test.tsx

@@ -60,6 +60,7 @@ vi.mock('../../../shared/pageContext', () => ({
         book: '预约咨询',
         tryAi: '用我的照片试发型',
         browse: '先看发型模板',
+        edit: '编辑个人主页',
         yearUnit: '年',
         untitled: '发型师',
         stats: { experience: '经验', rating: '评分', reviews: '评价' },
@@ -146,6 +147,7 @@ describe('StylistProfilePage', () => {
       expect(screen.getByRole('heading', { name: 'YASUAKI OGAWA' })).toBeInTheDocument()
     })
     expect(screen.getByRole('link', { name: '预约咨询' })).toHaveAttribute('href', 'https://example.com/booking')
+    expect(screen.getByRole('link', { name: '编辑个人主页' })).toHaveAttribute('href', '/zh/profile/manage')
     expect(screen.getByText('22年')).toBeInTheDocument()
     expect(screen.getByText('柔和长发')).toBeInTheDocument()
     expect(screen.queryByText('顾客反馈')).not.toBeInTheDocument()
@@ -160,9 +162,21 @@ describe('StylistProfilePage', () => {
       expect(getPublicStylistProfile).toHaveBeenCalledWith('yasuaki-ogawa')
     })
     expect(screen.getByText('层次剪发')).toBeInTheDocument()
+    expect(screen.queryByRole('link', { name: '编辑个人主页' })).not.toBeInTheDocument()
     expect(screen.queryByText('使用次数')).not.toBeInTheDocument()
   })
 
+  it('lets a logged-in stylist open the profile editor from a live slug page', async () => {
+    isAuthenticated.mockReturnValue(true)
+    getPublicStylistProfile.mockResolvedValue(liveProfile)
+    renderProfile('/zh/profile/yasuaki-ogawa')
+
+    await waitFor(() => {
+      expect(screen.getByRole('heading', { name: 'YASUAKI OGAWA' })).toBeInTheDocument()
+    })
+    expect(screen.getByRole('link', { name: '编辑个人主页' })).toHaveAttribute('href', '/zh/profile/manage')
+  })
+
   it('falls back to the demo page when the logged-in session has expired', async () => {
     isAuthenticated.mockReturnValue(true)
     getStylistMe.mockRejectedValue(new ApiError('UNAUTHORIZED', '登录已过期', 401))

+ 1 - 0
frontend/src/pages/dashboard/__tests__/DashboardPage.test.tsx

@@ -68,6 +68,7 @@ describe('DashboardPage', () => {
     )
 
     expect(screen.queryByText(/今日预约 .* 件 \/ 待跟进 .* 件/i)).not.toBeInTheDocument()
+    expect(screen.getByRole('link', { name: '个人主页' })).toHaveAttribute('href', '/zh/profile/manage')
 
     const entryLinks = screen.getAllByRole('link').filter((element) => {
       const href = element.getAttribute('href') || ''