/opt/canhelp/apps/web/src/app/tasks/new
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 */}
{/* Sidebar — Top specialists */}
)
}