// 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.')}
} onClick={() => { setIdx(0); setHistory([]); }}>{L('Ricomincia', 'Restart')}
) : (
[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 });