/opt/canhelp/apps/web/src/app/profile
NameSizeModeActions
page.tsx1433810644editdlrm
Edit: /opt/canhelp/apps/web/src/app/profile/page.tsx (143381B)
'use client' import { useState, useEffect, useRef, Suspense } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import { useSession, authClient } from '@/lib/auth' import { useLocale } from '@/context/locale' import * as api from '@/lib/api' import type { BalanceTransaction, PlanHistoryEntry, PortfolioItem, SpecialistCard, PriceListGroup } from '@/lib/api' import { CategoryIcon, isCategoryIconUrl } from '@/components/CategoryIcon' type Tab = 'info' | 'finance' | 'cards' | 'notifications' | 'reviews' | 'referral' | 'auto_response' | 'site' // ─── SpecialistCardEditor ────────────────────────────────────────────────── type CardTab = 'main' | 'skills' | 'categories' | 'geography' | 'portfolio' | 'pricelist' function SpecialistCardEditor({ card, isOpen, onToggle, onUpdate, onDelete, categories, locationTree, catLocale, locale, hasPriceList, visibilityIssues = [], moderationNotice = null, }: { card: SpecialistCard isOpen: boolean onToggle: () => void onUpdate: (card: SpecialistCard) => void onDelete: () => void categories: any[] locationTree: any[] catLocale: string locale: string hasPriceList: boolean visibilityIssues?: string[] moderationNotice?: { title: string; body: string | null; tone: 'amber' | 'green' | 'red' } | null }) { const { t } = useLocale() const hasVisibilityIssues = visibilityIssues.length > 0 const [cardTab, setCardTab] = useState('main') const [saving, setSaving] = useState(false) const [savedOk, setSavedOk] = useState(false) const [localTitle, setLocalTitle] = useState(card.title) const [localDescEl, setLocalDescEl] = useState(card.descriptionEl ?? '') const [localDescEn, setLocalDescEn] = useState(card.descriptionEn ?? '') const [localDescRu, setLocalDescRu] = useState(card.descriptionRu ?? '') const [localDescUk, setLocalDescUk] = useState(card.descriptionUk ?? '') const [localSkills, setLocalSkills] = useState(card.skills ?? []) const [skillInput, setSkillInput] = useState('') const [localCategories, setLocalCategories] = useState(card.categories ?? []) const [localLocations, setLocalLocations] = useState(card.locations ?? []) const [localPublicationStatus, setLocalPublicationStatus] = useState<'pending' | 'active' | 'inactive'>( card.publicationStatus ?? (card.isActive ? 'active' : 'inactive'), ) const [apiSkillSugs, setApiSkillSugs] = useState([]) const [portfolio, setPortfolio] = useState(card.portfolio ?? []) const [portfolioUploading, setPortfolioUploading] = useState(false) const [editingItem, setEditingItem] = useState(null) const [editTitle, setEditTitle] = useState('') const [editDesc, setEditDesc] = useState('') const portfolioFileRef = useRef(null) // Price list state const [priceList, setPriceList] = useState([]) const [priceListLoading, setPriceListLoading] = useState(false) const [newGroupTitle, setNewGroupTitle] = useState('') const [addingGroupId, setAddingGroupId] = useState(null) const [newItem, setNewItem] = useState<{ name: string; description: string; price: string; unit: string }>({ name: '', description: '', price: '', unit: '' }) const [editingGroupId, setEditingGroupId] = useState(null) const [editGroupTitle, setEditGroupTitle] = useState('') useEffect(() => { if (cardTab !== 'pricelist' || priceList.length > 0 || priceListLoading) return setPriceListLoading(true) api.getPriceList(card.id, locale) .then(setPriceList) .catch(() => {}) .finally(() => setPriceListLoading(false)) }, [cardTab, locale]) async function handleAddGroup() { if (!newGroupTitle.trim()) return const group = await api.createPriceListGroup(card.id, { title: newGroupTitle.trim(), locale: (['el', 'en', 'ru', 'uk'] as const).includes(locale as any) ? (locale as 'el' | 'en' | 'ru' | 'uk') : 'el', autoTranslate: true, }) setPriceList((prev) => [...prev, group]) setNewGroupTitle('') } async function handleDeleteGroup(groupId: string) { await api.deletePriceListGroup(groupId) setPriceList((prev) => prev.filter((g) => g.id !== groupId)) } async function handleSaveGroupTitle(groupId: string) { const updated = await api.updatePriceListGroup(groupId, { title: editGroupTitle.trim(), locale: (['el', 'en', 'ru', 'uk'] as const).includes(locale as any) ? (locale as 'el' | 'en' | 'ru' | 'uk') : 'el', autoTranslate: true, }) setPriceList((prev) => prev.map((g) => g.id === groupId ? { ...g, title: updated.title } : g)) setEditingGroupId(null) } async function handleAddItem(groupId: string) { if (!newItem.name.trim()) return const item = await api.createPriceListItem(groupId, { name: newItem.name.trim(), description: newItem.description.trim() || null, price: newItem.price.trim() || null, unit: newItem.unit.trim() || null, locale: (['el', 'en', 'ru', 'uk'] as const).includes(locale as any) ? (locale as 'el' | 'en' | 'ru' | 'uk') : 'el', autoTranslate: true, }) setPriceList((prev) => prev.map((g) => g.id === groupId ? { ...g, items: [...g.items, item] } : g)) setNewItem({ name: '', description: '', price: '', unit: '' }) setAddingGroupId(null) } async function handleDeleteItem(itemId: string, groupId: string) { await api.deletePriceListItem(itemId) setPriceList((prev) => prev.map((g) => g.id === groupId ? { ...g, items: g.items.filter((i) => i.id !== itemId) } : g)) } useEffect(() => { if (cardTab !== 'skills') return api .getSkillSuggestions(localCategories.length > 0 ? localCategories : undefined) .then(setApiSkillSugs) .catch(() => setApiSkillSugs([])) }, [localCategories, cardTab]) const lbl = (ru: string, en: string, el: string) => locale === 'ru' ? ru : locale === 'en' ? en : el async function saveAll() { setSaving(true) try { const activeLocale = (['el', 'en', 'ru', 'uk'] as const).includes(locale as any) ? (locale as 'el' | 'en' | 'ru' | 'uk') : 'el' const cap = activeLocale.charAt(0).toUpperCase() + activeLocale.slice(1) as 'El' | 'En' | 'Ru' | 'Uk' const descField = `description${cap}` as const const descValue = { el: localDescEl, en: localDescEn, ru: localDescRu, uk: localDescUk }[activeLocale] || null const updated = await api.updateSpecialistCard(card.id, { title: localTitle, [descField]: descValue, descriptionLocale: activeLocale, autoTranslate: !!descValue, skills: localSkills, categories: localCategories, locations: localLocations, publicationStatus: localPublicationStatus, isActive: localPublicationStatus === 'active', }) onUpdate({ ...updated, portfolio }) setSavedOk(true) setTimeout(() => setSavedOk(false), 2000) } catch {} finally { setSaving(false) } } async function updatePublicationStatus(next: 'pending' | 'active' | 'inactive') { setLocalPublicationStatus(next) try { await api.updateSpecialistCard(card.id, { publicationStatus: next, isActive: next === 'active', }) } catch {} } function addSkill(s: string) { const trimmed = s.trim() if (trimmed && !localSkills.includes(trimmed)) setLocalSkills([...localSkills, trimmed]) setSkillInput('') } function toggleCategory(slug: string, childSlugs: string[] = []) { if (localCategories.includes(slug)) { setLocalCategories(localCategories.filter((c) => c !== slug && !childSlugs.includes(c))) } else { setLocalCategories([...new Set([...localCategories, slug, ...childSlugs])]) } } function toggleLocation(slug: string, childSlugs: string[] = []) { if (localLocations.includes(slug)) { setLocalLocations(localLocations.filter((c) => c !== slug && !childSlugs.includes(c))) } else { setLocalLocations([...new Set([...localLocations, slug, ...childSlugs])]) } } async function handlePortfolioUpload(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return setPortfolioUploading(true) try { const form = new FormData() form.append('file', file) form.append('cardId', card.id) const res = await fetch( `${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000'}/api/portfolio`, { method: 'POST', credentials: 'include', body: form }, ) if (!res.ok) throw new Error('Upload failed') const item: PortfolioItem = await res.json() const next = [...portfolio, item] setPortfolio(next) onUpdate({ ...card, portfolio: next }) } catch {} finally { setPortfolioUploading(false) if (portfolioFileRef.current) portfolioFileRef.current.value = '' } } async function handlePortfolioDelete(id: string) { try { await api.deletePortfolioItem(id) const next = portfolio.filter((p) => p.id !== id) setPortfolio(next) onUpdate({ ...card, portfolio: next }) } catch {} } async function handlePortfolioSave(id: string) { try { const item = await api.updatePortfolioItem(id, { title: editTitle, description: editDesc }) setPortfolio((prev) => prev.map((p) => (p.id === id ? item : p))) setEditingItem(null) } catch {} } const cardTabs: { id: CardTab; label: string; hidden?: boolean }[] = [ { id: 'main', label: t('profile.card.tab.main') }, { id: 'categories', label: t('profile.card.tab.categories') }, { id: 'skills', label: t('profile.card.tab.skills') }, { id: 'geography', label: t('profile.card.tab.geography') }, { id: 'pricelist', label: t('profile.card.tab.pricelist'), hidden: !hasPriceList }, { id: 'portfolio', label: t('profile.card.tab.portfolio') }, ] const pendingReviewNotice = localPublicationStatus === 'pending' ? { title: t('profile.card.review_pending', 'Pending approval'), body: t('profile.card.review_pending_hint', 'Your card is being reviewed by the admin team.'), tone: 'amber' as const, } : null const effectiveModerationNotice = moderationNotice ?? pendingReviewNotice return (
{/* Card header */}
{/* Delete */}
{isOpen && (
{hasVisibilityIssues && (

{t('profile.card.visibility_issue', 'Card is not shown in offers')}

    {visibilityIssues.map((issue) => (
  • {issue}
  • ))}
)} {effectiveModerationNotice && (

{effectiveModerationNotice.title}

{effectiveModerationNotice.body && (

{effectiveModerationNotice.body}

)}
)} {/* Inner tabs */}
{cardTabs.filter((t) => !t.hidden).map((t) => ( ))}
{/* ── MAIN ── */} {cardTab === 'main' && (
setLocalTitle(e.target.value)} maxLength={200} placeholder={t('profile.cards.new_title')} className="w-full border border-gray-300 rounded-lg px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-blue-400" /> {/* Single-locale description — auto-translated to other languages on save */} {(() => { const activeLocale = (['el', 'en', 'ru', 'uk'] as const).includes(locale as any) ? (locale as 'el' | 'en' | 'ru' | 'uk') : 'el' const descMap = { el: [localDescEl, setLocalDescEl] as const, en: [localDescEn, setLocalDescEn] as const, ru: [localDescRu, setLocalDescRu] as const, uk: [localDescUk, setLocalDescUk] as const } const [val, setVal] = descMap[activeLocale] const flags: Record = { el: '🇬🇷', en: '🇬🇧', ru: '🇷🇺', uk: '🇺🇦' } const otherLocales = (['el', 'en', 'ru', 'uk'] as const).filter(l => l !== activeLocale) const otherHasContent = otherLocales.some(l => descMap[l][0]) return (