// Prodz — Shoutouts: discovery of opportunities (collab + job call) as a swipe // stack. Three actions: Salva · Salta · Rispondi. Undo + saved queue always // visible (brief §1.5 / §2.4). "Rispondi" opens a conversation in the Inbox. function Shoutouts() { const { L } = useL(); const nav = useNav(); const { Button, Tag, Avatar } = window.ProdzDesignSystem_d0b87b; const deck = SHOUTOUTS; const [idx, setIdx] = useStateS(0); const [saved, setSaved] = useStateS([]); const [history, setHistory] = useStateS([]); const [drag, setDrag] = useStateS({ dx: 0, dy: 0, active: false }); const [leaving, setLeaving] = useStateS(null); const [showSaved, setShowSaved] = useStateS(false); const startRef = useRefS(null); const undo = () => { setHistory((h) => { if (!h.length) return h; const last = h[h.length - 1]; setIdx((i) => Math.max(0, i - 1)); if (last.wasSaved) setSaved((s) => s.filter((c) => c.id !== last.card.id)); return h.slice(0, -1); }); }; const commit = (action) => { if (idx >= deck.length || leaving) return; const card = deck[idx]; const fly = action === 'save' ? { x: 560, y: -30, rot: 20 } : action === 'skip' ? { x: -560, y: -30, rot: -20 } : { x: 0, y: -760, rot: 0 }; setLeaving({ dir: action, ...fly }); const wasSaved = action === 'save'; setTimeout(() => { setHistory((h) => [...h, { action, card, wasSaved }]); if (wasSaved) setSaved((s) => (s.find((c) => c.id === card.id) ? s : [...s, card])); setIdx((i) => i + 1); setLeaving(null); setDrag({ dx: 0, dy: 0, active: false }); if (action === 'reply') nav.replyShoutout(card); else nav.toast(action === 'save' ? L('Salvato nella coda', 'Saved to queue') : L('Saltato', 'Skipped'), L('Annulla', 'Undo'), undo); }, 300); }; const onDown = (e) => { if (leaving) return; startRef.current = { x: e.clientX, y: e.clientY }; setDrag({ dx: 0, dy: 0, active: true }); try { e.currentTarget.setPointerCapture(e.pointerId); } catch (_) {} }; const onMove = (e) => { if (!drag.active || !startRef.current) return; setDrag({ dx: e.clientX - startRef.current.x, dy: e.clientY - startRef.current.y, active: true }); }; const onUp = () => { if (!drag.active) return; const { dx, dy } = drag; const TH = 105; if (dy < -150 && Math.abs(dx) < 100) commit('reply'); else if (dx > TH) commit('save'); else if (dx < -TH) commit('skip'); else setDrag({ dx: 0, dy: 0, active: false }); startRef.current = null; }; const remaining = deck.length - idx; const topTransform = leaving ? `translate(${leaving.x}px, ${leaving.y}px) rotate(${leaving.rot}deg)` : `translate(${drag.dx}px, ${drag.dy}px) rotate(${drag.dx * 0.04}deg)`; const topTransition = (leaving || !drag.active) ? 'transform 300ms var(--ease-out)' : 'none'; // direction hints const dirSave = Math.max(0, Math.min(1, drag.dx / 110)); const dirSkip = Math.max(0, Math.min(1, -drag.dx / 110)); const dirReply = Math.max(0, Math.min(1, -drag.dy / 150)) * (Math.abs(drag.dx) < 100 ? 1 : 0); return (

Shoutouts

{L('Scopri collab e job call. Scorri o usa i tasti.', 'Discover collabs & job calls. Swipe or tap.')}

{/* saved queue — always visible */}
{/* card stack */}
{remaining <= 0 ? (
{L('Sei in pari', 'All caught up')}
{L('Hai visto tutte le opportunità per ora. Torna più tardi.', 'You’ve seen every opportunity for now. Check back later.')}
) : ( [2, 1, 0].map((depth) => { const i = idx + depth; if (i >= deck.length) return null; const card = deck[i]; const isTop = depth === 0; const style = isTop ? { transform: topTransform, transition: topTransition, zIndex: 3, cursor: drag.active ? 'grabbing' : 'grab' } : { transform: `translateY(${depth * 12}px) scale(${1 - depth * 0.05})`, transition: 'transform 300ms var(--ease-out)', zIndex: 3 - depth, opacity: depth === 2 ? 0.55 : 0.85 }; return (
{isTop && }
); }) )}
{/* actions */} {remaining > 0 && (
commit('skip')} bg="var(--surface-raised)" color="var(--text-muted)" size={56} label={L('Salta', 'Skip')} /> commit('reply')} bg="var(--prodz-gold)" color="var(--text-on-accent)" size={66} glow="var(--glow-gold)" label={L('Rispondi', 'Reply')} /> commit('save')} bg="var(--teal-surface)" color="var(--prodz-teal)" size={56} ring="var(--ring-teal)" label={L('Salva', 'Save')} /> {history.length > 0 && }
)} setShowSaved(false)}> {L('Coda dei salvati', 'Saved queue')} · {saved.length} {saved.length === 0 ? (

{L('Nessuna opportunità salvata. Scorri a destra o tocca Salva.', 'No saved opportunities yet. Swipe right or tap Save.')}

) : (
{saved.map((c) => (
{tr(L, c.title)}
{c.author} · {c.city}
))}
)}
); } function ShoutoutCard({ card, L }) { const { Tag, Avatar } = window.ProdzDesignSystem_d0b87b; const warm = card.surface === 'cream' || card.surface === 'mint'; const bg = card.surface === 'cream' ? 'var(--cream)' : card.surface === 'mint' ? 'var(--mint)' : 'var(--surface-card)'; const ink = warm ? (card.surface === 'cream' ? 'var(--cream-ink)' : 'var(--mint-ink)') : 'var(--text-primary)'; const sub = warm ? 'rgba(58,47,36,0.7)' : 'var(--text-muted)'; const kindLabel = card.kind === 'job' ? L('Job Call', 'Job Call') : 'Collab Proposal'; return (
{kindLabel} {card.paid && {L('Retribuito', 'Paid')}}
{card.author}
{tr(L, card.role)} · {card.city}
{tr(L, card.title)}
{tr(L, card.blurb)}
{card.tags.map((t) => {t})}
{tr(L, card.date)}
); } function SwipeHints({ save, skip, reply, L }) { const tag = (label, color, opacity, pos) => ( {label} ); return ( <> {tag(L('Salva', 'Save'), 'var(--prodz-teal)', save, { top: 22, left: 22, transformOrigin: 'left top' })} {tag(L('Salta', 'Skip'), '#ff6b6b', skip, { top: 22, right: 22, transformOrigin: 'right top' })} {tag(L('Rispondi', 'Reply'), 'var(--prodz-gold)', reply, { bottom: 80, left: '50%', marginLeft: -54 })} ); } function ActionBtn({ icon, onClick, bg, color, size, glow, ring, label, ghost }) { return ( ); } const savedChip = { display: 'inline-flex', alignItems: 'center', gap: 7, padding: '8px 12px', background: 'var(--surface-card)', border: 'none', cursor: 'pointer', borderRadius: 'var(--radius-pill)', boxShadow: 'var(--ring-inset-hairline)' }; const emptyState = { position: 'absolute', inset: 0, display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }; Object.assign(window, { Shoutouts });