/opt/canhelp/apps/web/src/app/tasks/new
NameSizeModeActions
page.tsx250760644editdlrm
Edit: /opt/canhelp/apps/web/src/app/tasks/new/page.tsx (25076B)
'use client' import { useState, useEffect, useRef } from 'react' import { useRouter, useSearchParams } from 'next/navigation' import Link from 'next/link' import { useSession } from '@/lib/auth' import { useLocale } from '@/context/locale' import { useLocation } from '@/context/location' import { createTask, getCategories, getLocations, getSpecialists } from '@/lib/api' import { formatShortName } from '@/lib/formatName' import { formatLastSeen } from '@/lib/formatLastSeen' import type { Category } from '@canhelp/shared' import { TaskLocationPicker } from '@/components/TaskLocationPicker' import { TaskCategoryPicker } from '@/components/TaskCategoryPicker' import { TurnstileWidget } from '@/components/Turnstile' import { CAPTCHA_ENABLED } from '@/components/Turnstile' const API = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:4000' const localISO = (d: Date) => { const y = d.getFullYear() const m = String(d.getMonth() + 1).padStart(2, '0') const day = String(d.getDate()).padStart(2, '0') return `${y}-${m}-${day}` } export default function CreateTaskPage() { const { data: session, isPending } = useSession() const router = useRouter() const searchParams = useSearchParams() const preselectedCategory = searchParams.get('category') ?? '' const { t, locale } = useLocale() const [categories, setCategories] = useState([]) const [locations, setLocations] = useState([]) const [loading, setLoading] = useState(false) const [error, setError] = useState('') const [topSpecialists, setTopSpecialists] = useState([]) const [deadline, setDeadline] = useState(() => { const d = new Date() d.setDate(d.getDate() + 1) return localISO(d) }) const [expiresAtDays, setExpiresAtDays] = useState(30) const [calendarMonth, setCalendarMonth] = useState(() => { const d = new Date() d.setDate(d.getDate() + 1) return localISO(d).slice(0, 7) }) const calendarInputRef = useRef(null) const [specialistAvailMap, setSpecialistAvailMap] = useState>({}) const [invitedIds, setInvitedIds] = useState>(new Set()) const [captchaToken, setCaptchaToken] = useState(CAPTCHA_ENABLED ? '' : 'dev') const { locationSlug: ctxLocation } = useLocation() const [selectedLocation, setSelectedLocation] = useState('') const [selectedCategory, setSelectedCategory] = useState(preselectedCategory) useEffect(() => { if (ctxLocation) setSelectedLocation(ctxLocation) }, [ctxLocation]) const [images, setImages] = useState([]) const [photoUploading, setPhotoUploading] = useState(false) const fileInputRef = useRef(null) useEffect(() => { if (!isPending && !session) router.push('/login') getCategories().then(setCategories).catch(() => {}) getLocations().then(setLocations).catch(() => {}) }, [session, isPending]) useEffect(() => { if (!selectedCategory) { setTopSpecialists([]) return } getSpecialists({ limit: '5', category: selectedCategory, locale }).then((r) => setTopSpecialists(r.data)).catch(() => {}) }, [selectedCategory, locale]) useEffect(() => { if (!deadline || topSpecialists.length === 0) return const month = deadline.slice(0, 7) const ids = topSpecialists.map((u: any) => u.id) Promise.all( ids.map((id: string) => fetch(`${API}/api/availability/user/${id}?month=${month}`, { credentials: 'include' }) .then((r) => r.ok ? r.json() : []) .then((rows: { date: string; status: string }[]) => { const row = rows.find((r) => r.date === deadline) return { id, status: (row?.status ?? null) as 'available' | 'busy' | null } }) .catch(() => ({ id, status: null as null })) ) ).then((results) => { const map: Record = {} results.forEach(({ id, status }) => { map[id] = status }) setSpecialistAvailMap(map) }) }, [deadline, topSpecialists]) async function handlePhotoChange(e: React.ChangeEvent) { const file = e.target.files?.[0] if (!file) return setPhotoUploading(true) try { const form = new FormData() form.append('file', file) const res = await fetch(`${API}/api/uploads`, { method: 'POST', credentials: 'include', body: form }) if (!res.ok) throw new Error('Upload failed') const { url } = await res.json() setImages((prev) => [...prev, `${API}${url}`]) } catch { setError(t('profile.upload.error')) } finally { setPhotoUploading(false) if (fileInputRef.current) fileInputRef.current.value = '' } } async function handleSubmit(e: React.FormEvent) { e.preventDefault() setError('') setLoading(true) const form = new FormData(e.currentTarget) const data: any = { title: form.get('title') as string, description: form.get('description') as string, category: form.get('category') as string || undefined, location: form.get('location') as string || undefined, budget: form.get('budget') ? Number(form.get('budget')) : undefined, budgetNegotiable: (form.get('budgetNegotiable') === 'on'), deadline: form.get('deadline') ? new Date(form.get('deadline') as string).toISOString() : undefined, expiresAt: (() => { const d = new Date(); d.setDate(d.getDate() + Number(form.get('expiresAtDays') || 30)); return d.toISOString() })(), timeSlot: form.get('timeSlot') as string || undefined, district: form.get('district') as string || undefined, street: form.get('street') as string || undefined, houseNumber: form.get('houseNumber') as string || undefined, confidentialNote: form.get('confidentialNote') as string || undefined, images: images.length > 0 ? images : undefined, status: 'open' as const, locale: locale as 'el' | 'en' | 'uk' | 'ru', inviteSpecialistIds: invitedIds.size > 0 ? [...invitedIds] : undefined, } try { const task = await createTask({ ...data, captchaToken } as any) router.push(`/tasks/${task.id}`) } catch (err: any) { setError(err.message || t('create.error')) } finally { setLoading(false) } } if (isPending) return
{t('common.loading')}
const catLocale = locale === 'en' ? 'en' : locale === 'ru' ? 'ru' : locale === 'uk' ? 'uk' : 'el' const dateLocale = locale === 'ru' ? 'ru-RU' : locale === 'uk' ? 'uk-UA' : locale === 'en' ? 'en-US' : 'el-GR' const todayMidnight = new Date() todayMidnight.setHours(0, 0, 0, 0) const [cmYear, cmMonth] = calendarMonth.split('-').map(Number) const daysInMonth = new Date(cmYear, cmMonth, 0).getDate() const calendarDates: Date[] = [] for (let day = 1; day <= daysInMonth; day++) { const d = new Date(cmYear, cmMonth - 1, day) if (d >= todayMidnight) calendarDates.push(d) } const monthOptions = Array.from({ length: 12 }, (_, i) => { const d = new Date() d.setDate(1) d.setMonth(d.getMonth() + i) return { value: localISO(d).slice(0, 7), label: d.toLocaleDateString(dateLocale, { month: 'long', year: 'numeric' }), } }) const sidebarTitle = t('tasks.new.top_specialists') return (

{t('create.title')}

{/* Main form */}
{error && (
{error}
)}