import { useEffect, useState } from 'react'; import { useParams } from 'react-router-dom'; import Typography from 'antd/es/typography'; import Card from 'antd/es/card'; import Upload from 'antd/es/upload'; import Button from 'antd/es/button'; import Input from 'antd/es/input'; import Alert from 'antd/es/alert'; import Spin from 'antd/es/spin'; import Space from 'antd/es/space'; import Checkbox from 'antd/es/checkbox'; import { UploadOutlined } from '@ant-design/icons'; import type { UploadFile } from 'antd/es/upload/interface'; import { fetchIntakeLink, submitItem } from './intakeApi'; import type { LinkState } from './intakeApi'; const { Title, Paragraph } = Typography; const { TextArea } = Input; /** * Where someone with no account sends in photos of one item (#222). * * The three types the server will accept, and the same per-request cap. Listed * here so the file picker offers exactly what will be taken and the count is * bounded before anything is uploaded — but the server checks both again, * because everything on this page is under the sender's control. */ const ACCEPT = 'image/jpeg,image/png,image/webp'; const MAX_IMAGES = 6; export default function Submit() { const { token = '' } = useParams(); const [state, setState] = useState({ kind: 'unusable' }); const [checking, setChecking] = useState(true); const [files, setFiles] = useState([]); const [note, setNote] = useState(''); // Ticked by default. Most items look better cut out, and a submitter who // wants their kitchen table in the photograph can say so — the reverse // default would mean almost nobody got it. const [removeBackground, setRemoveBackground] = useState(true); const [sending, setSending] = useState(false); const [sent, setSent] = useState(false); const [error, setError] = useState(null); useEffect(() => { let cancelled = false; void fetchIntakeLink(token).then((result) => { // The token can change if the URL does, and a late response from the // previous one would otherwise overwrite the current answer. if (cancelled) return; setState(result); setChecking(false); }); return () => { cancelled = true; }; }, [token]); async function send() { setSending(true); setError(null); const result = await submitItem( token, // originFileObj is what antd hands back for a file it did not upload // itself; beforeUpload returning false is what keeps them here. flatMap // rather than map-then-filter because a type predicate cannot narrow to // File here — antd's RcFile extends it, so the predicate would widen. files.flatMap((f) => (f.originFileObj ? [f.originFileObj] : [])), note, removeBackground ); setSending(false); if (result.ok) { setSent(true); return; } setError(result.error); } if (checking) { return (
); } // Kept apart from the state below on purpose. The two need opposite // reactions — wait a moment, versus go and ask for a different link — so // telling a throttled sender their link was dead would send them to fetch a // replacement that could not have helped. if (state.kind === 'throttled') { return (
Too many requests just now Your link is fine — this page has just been asked for too many times from your connection. Wait a minute and reload.
); } // One state for unknown, revoked and used-up alike, matching the server's // single 404. Saying which it was would tell a stranger whether a link they // guessed at exists. if (state.kind === 'unusable') { return (
This link is not active It may have been turned off, or already used as many times as it was meant for. Ask whoever sent it to you for a new one.
); } if (sent) { return (
Thank you — it arrived Somebody will look at your photos and write it up. Nothing is listed for sale until they have.
); } return (
Send in an item Photos of one item, and anything you know about it. Send each item separately. false} onChange={({ fileList }) => setFiles(fileList)} >