import React, {useCallback, useEffect, useRef, useState} from 'react'; import {getJson, postJson} from '../api'; import Modal from './Modal'; import Steps from './Steps'; // Flickr's own sign-in can't be shown inside the dialog, so it opens in a small // window, and lands on a page that closes itself (see AuthenticationsController#connected). const CONNECT_URL = `/members/auth/flickr?origin=${encodeURIComponent('/authentications/connected')}`; const POLL_MS = 2000; function errorMessages(status, data) { if (status === 422 && data && data.errors) { const messages = Object.entries(data.errors).flatMap(([field, list]) => list.map((m) => `${field.replace(/_/g, ' ').replace(/^./, (c) => c.toUpperCase())} ${m}`)); if (messages.length > 0) return messages; } if (status === 401) return ['Please sign in again to add this photo.']; return ['Something went wrong adding that photo. Please try again.']; } // Adding a photo to a planting or garden, as a dialog over the garden list. The // photos are the member's own, from Flickr, so if Flickr isn't connected (or the // connection has expired) the first step connects it, here, in a pop-up; the dialog // notices when that has happened. Then choose a photo (by album or tag, a page at a // time), confirm it, and it is added and the garden's card refreshed, so the page // is never left. `newUrl` is the menu item's own link, /photos/new?type=..&id=... export default function AddPhotoModal({label, newUrl, iconUrl, onClose, onAdded}) { const target = new URL(newUrl, window.location.origin).searchParams; const type = target.get('type'); const id = target.get('id'); const [data, setData] = useState(null); // {state: connect | reconnect | ready, photos, sets, page, total_pages, ...} const [filters, setFilters] = useState({set: '', tag: '', page: 1}); const [tagInput, setTagInput] = useState(''); const [loading, setLoading] = useState(true); const [loadError, setLoadError] = useState(false); const [connectFirst, setConnectFirst] = useState(false); // opened without Flickr connected const [waiting, setWaiting] = useState(false); // the connect window is open const [blocked, setBlocked] = useState(false); // ...but the browser would not open it const [chosen, setChosen] = useState(null); const [saving, setSaving] = useState(false); const [errors, setErrors] = useState([]); const popup = useRef(null); const load = useCallback(async (next, signal) => { const query = new URLSearchParams({type, id}); if (next.set) query.set('set', next.set); if (next.tag) query.set('tag', next.tag); if (next.page > 1) query.set('page', next.page); const {ok, data: result} = await getJson(`/photos/new.json?${query}`, {signal}); if (!ok) throw new Error('load failed'); return result; }, [type, id]); useEffect(() => { const controller = new AbortController(); setLoading(true); (async () => { try { const result = await load(filters, controller.signal); setData(result); setLoadError(false); if (result.state !== 'ready') setConnectFirst(true); } catch (error) { if (error.name !== 'AbortError') setLoadError(true); } if (!controller.signal.aborted) setLoading(false); })(); return () => controller.abort(); }, [filters, load]); // While the connect window is open, ask the server whether it worked. useEffect(() => { if (!waiting) return undefined; const timer = setInterval(async () => { try { const result = await load(filters); if (result.state === 'ready') { setData(result); setWaiting(false); if (popup.current && !popup.current.closed) popup.current.close(); } } catch (error) { // Try again at the next tick. } }, POLL_MS); return () => clearInterval(timer); }, [waiting, filters, load]); function connect() { popup.current = window.open(CONNECT_URL, 'flickr-connect', 'width=640,height=760'); setBlocked(!popup.current); setWaiting(true); } function search(event) { event.preventDefault(); setFilters({set: '', tag: tagInput.trim(), page: 1}); } function chooseAlbum(event) { setTagInput(''); setFilters({set: event.target.value, tag: '', page: 1}); } async function add() { setSaving(true); setErrors([]); try { const {ok, status, data: result} = await postJson(`/photos.json?type=${encodeURIComponent(type)}&id=${encodeURIComponent(id)}`, { photo: {source_id: chosen.id, source: 'flickr'}, }); if (ok) { onAdded(result); // closes this dialog return; } setErrors(errorMessages(status, result)); } catch (error) { setErrors(['Couldn\'t reach the server. Please try again.']); } setSaving(false); } const ready = data && data.state === 'ready'; const steps = connectFirst ? ['Connect Flickr', 'Choose a photo', 'Confirm'] : ['Choose a photo', 'Confirm']; const firstChoose = connectFirst ? 2 : 1; const current = !ready ? 1 : chosen ? firstChoose + 1 : firstChoose; const title = ( <> {iconUrl && } Add a photo to {label} ); return ( {loadError && (
Couldn’t load your photos. Please close this and try again.
)} {!loadError && !data && (