PromptBase
Upgrade
Close icon
General
Home
Marketplace
Create
Hire
Login
Chat
Sell
Explore

Prompt Details

Model
(claude-4-7-opus)
Token size
266
Example input
[ITEM_TYPE]: job listings [FIELD_LIST]: title, company, location, salaryRange, remote, logoUrl [STYLING_APPROACH]: Tailwind CSS [LIKE_ACTION]: POST to /api/applications to apply for the job [SKIP_ACTION]: POST to /api/dismissed to hide the job
Example output
import { useState, useRef, useCallback } from "react"; import { useSpring, animated, to as interpolate } from "@react-spring/web"; import { useDrag } from "@use-gesture/react"; // ─── Types ─────────────────────────────────────────────────────────────────── export interface JobListing { id: string; title: string; company: string; location: string; salaryRange: string; remote: boolean; logoUrl: string; } type SwipeDirection = "left" | "right"; interface CardSpringProps { x: number; y: number; scale: number; rotation: number; opacity: number; } // ─── Constants ──────────────────────────────────────────────────────────────── const SWIPE_THRESHOLD_PX = 100; const SWIPE_THRESHOLD_VEL = 0.5; const FLY_OUT_X = 800; const STACK_OFFSETS = [ { y: 0, scale: 1 }, { y: 12, scale: 0.95 }, { y: 22, scale: 0.91 }, ] as const; // ─── Helpers ────────────────────────────────────────────────────────────────── function clamp(val: number, min: number, max: number) { return Math.min(Math.max(val, min), max); } function rotationFromDrag(x: number) { return x / 20; } function stampOpacity(x: number) { return clamp(Math.abs(x) / SWIPE_THRESHOLD_PX, 0, 1); } function haptic() { if ("vibrate" in navigator) navigator.vibrate(30); } async function postAction(url: string, job: JobListing) { try { await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ jobId: job.id }), }); } catch { // silently fail — optimistic UI } } // ─── Job Card (single animated card) ───────────────────────────────────────── interface JobCardProps { job: JobListing; stackIndex: number; // 0 = front isTop: boolean; onSwipe: (job: JobListing, dir: SwipeDirection) => void; dragX: number; setDragX: (x: number) => void; } function JobCard({ job, stackIndex, isTop, onSwipe, dragX, setDragX }: JobCardProps) { const offset = STACK_OFFSETS[stackIndex] ?? STACK_OFFSETS[2]; const isDragging = useRef(false); const [spring, api] = useSpring<CardSpringProps>(() => ({ x: 0, y: offset.y, scale: offset.scale, rotation: 0, opacity: 1, config: { tension: 300, friction: 28 }, })); const triggerSwipe = useCallback( (dir: SwipeDirection) => { haptic(); api.start({ x: dir === "right" ? FLY_OUT_X : -FLY_OUT_X, y: 0, rotation: dir === "right" ? 30 : -30, opacity: 0, config: { tension: 240, friction: 22 }, onRest: () => onSwipe(job, dir), }); }, [api, job, onSwipe] ); // Expose imperative swipe for button controls (JobCard as any)[`swipe_${job.id}`] = triggerSwipe; const bind = useDrag( ({ active, movement: [mx], velocity: [vx], last }) => { isDragging.current = active; if (active) { setDragX(mx); api.start({ x: mx, y: offset.y, scale: offset.scale + (active ? 0.02 : 0), rotation: rotationFromDrag(mx), opacity: 1, immediate: (key) => key === "x" || key === "rotation", config: { tension: 300, friction: 28 }, }); } if (last) { setDragX(0); const shouldSwipe = Math.abs(mx) > SWIPE_THRESHOLD_PX || Math.abs(vx) > SWIPE_THRESHOLD_VEL; if (shouldSwipe) { const dir: SwipeDirection = mx > 0 ? "right" : "left"; triggerSwipe(dir); } else { api.start({ x: 0, y: offset.y, scale: offset.scale, rotation: 0, opacity: 1, config: { tension: 300, friction: 28 }, }); } } }, { enabled: isTop, filterTaps: true } ); const likeOpacity = isTop && dragX > 0 ? stampOpacity(dragX) : 0; const nopeOpacity = isTop && dragX < 0 ? stampOpacity(dragX) : 0; return ( <animated.div {...(isTop ? bind() : {})} style={{ position: "absolute", width: "100%", height: "100%", touchAction: "none", userSelect: "none", willChange: "transform", transform: interpolate( [spring.x, spring.y, spring.scale, spring.rotation], (x, y, s, r) => `translateX(${x}px) translateY(${y}px) scale(${s}) rotate(${r}deg)` ), opacity: spring.opacity, cursor: isTop ? "grab" : "default", }} > <div className="relative w-full h-full bg-white rounded-3xl overflow-hidden border border-gray-100 shadow-lg select-none"> {/* Logo header */} <div className="flex items-center gap-4 p-6 border-b border-gray-100"> <img src={job.logoUrl} alt={`${job.company} logo`} className="w-14 h-14 rounded-2xl object-cover border border-gray-100" draggable={false} onError={(e) => { (e.target as HTMLImageElement).src = `https://ui-avatars.com/api/?name=${encodeURIComponent(job.company)}&background=random&size=56`; }} /> <div className="flex-1 min-w-0"> <h3 className="font-semibold text-gray-900 text-lg leading-tight truncate"> {job.title} </h3> <p className="text-gray-500 text-sm mt-0.5">{job.company}</p> </div> </div> {/* Details */} <div className="p-6 flex flex-col gap-3"> <Detail icon="📍" text={job.location} /> <Detail icon="💰" text={job.salaryRange} /> {job.remote && ( <span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full bg-emerald-50 text-emerald-700 text-sm font-medium w-fit"> <span className="w-1.5 h-1.5 rounded-full bg-emerald-500 inline-block" /> Remote friendly </span> )} </div> {/* LIKE stamp */} <div aria-hidden="true" className="absolute top-8 left-8 pointer-events-none" style={{ opacity: likeOpacity, transform: "rotate(-15deg)" }} > <span className="text-3xl font-black tracking-widest text-emerald-500 border-4 border-emerald-500 rounded-xl px-3 py-1 uppercase"> Like </span> </div> {/* NOPE stamp */} <div aria-hidden="true" className="absolute top-8 right-8 pointer-events-none" style={{ opacity: nopeOpacity, transform: "rotate(15deg)" }} > <span className="text-3xl font-black tracking-widest text-rose-500 border-4 border-rose-500 rounded-xl px-3 py-1 uppercase"> Nope </span> </div> </div> </animated.div> ); } function Detail({ icon, text }: { icon: string; text: string }) { return ( <div className="flex items-center gap-2.5 text-gray-600 text-sm"> <span className="text-base" aria-hidden="true"> {icon} </span> <span>{text}</span> </div> ); } // ─── Empty State ────────────────────────────────────────────────────────────── function EmptyState({ onReset }: { onReset: () => void }) { return ( <div className="flex flex-col items-center justify-center h-full gap-6 text-center px-8"> <div className="text-6xl">🎉</div> <div> <h3 className="text-xl font-semibold text-gray-800 mb-2">All caught up!</h3> <p className="text-gray-500 text-sm">You've reviewed all available job listings.</p> </div> <button onClick={onReset} className="px-6 py-3 rounded-2xl bg-gray-900 text-white font-medium text-sm hover:bg-gray-700 active:scale-95 transition-all" > Reset cards </button> </div> ); } // ─── Action Buttons ─────────────────────────────────────────────────────────── interface ActionButtonsProps { onSkip: () => void; onLike: () => void; onUndo: () => void; canUndo: boolean; isEmpty: boolean; } function ActionButtons({ onSkip, onLike, onUndo, canUndo, isEmpty }: ActionButtonsProps) { if (isEmpty) return null; return ( <div className="flex items-center justify-center gap-5 pt-4"> <ActionBtn onClick={onSkip} label="Skip" color="rose" icon="✕" /> <button onClick={onUndo} disabled={!canUndo} aria-label="Undo last swipe" title="Undo" className="w-11 h-11 rounded-full bg-white border border-gray-200 text-gray-400 text-lg flex items-center justify-center shadow-sm disabled:opacity-30 hover:enabled:bg-gray-50 active:enabled:scale-95 transition-all" > ↩ </button> <ActionBtn onClick={onLike} label="Apply" color="emerald" icon="✓" /> </div> ); } function ActionBtn({ onClick, label, color, icon, }: { onClick: () => void; label: string; color: "rose" | "emerald"; icon: string; }) { const colorMap = { rose: "bg-rose-50 text-rose-500 border-rose-200 hover:bg-rose-100", emerald: "bg-emerald-50 text-emerald-600 border-emerald-200 hover:bg-emerald-100", }; return ( <button onClick={onClick} aria-label={label} className={`flex flex-col items-center gap-1 w-16 h-16 rounded-2xl border ${colorMap[color]} text-xl font-bold transition-all active:scale-95`} > <span className="mt-3">{icon}</span> <span className="text-[10px] font-medium">{label}</span> </button> ); } // ─── Main Component ─────────────────────────────────────────────────────────── export interface JobCardStackProps { jobs: JobListing[]; className?: string; } export function JobCardStack({ jobs: initialJobs, className = "" }: JobCardStackProps) { const [deck, setDeck] = useState<JobListing[]>(initialJobs); const [undoStack, setUndoStack] = useState<Array<{ job: JobListing; dir: SwipeDirection }>>([]); const [dragX, setDragX] = useState(0); const hasUsedUndo = useRef(false); const topJob = deck[deck.length - 1]; const isEmpty = deck.length === 0; const handleSwipe = useCallback( (job: JobListing, dir: SwipeDirection) => { postAction(dir === "right" ? "/api/applications" : "/api/dismissed", job); setDeck((prev) => prev.filter((j) => j.id !== job.id)); setUndoStack((prev) => { if (hasUsedUndo.current) return []; return [...prev, { job, dir }]; }); }, [] ); const imperativeSwipe = (dir: SwipeDirection) => { if (!topJob) return; const fn = (JobCard as any)[`swipe_${topJob.id}`]; if (fn) fn(dir); }; const handleUndo = () => { if (undoStack.length === 0 || hasUsedUndo.current) return; hasUsedUndo.current = true; const last = undoStack[undoStack.length - 1]; setDeck((prev) => [...prev, last.job]); setUndoStack([]); }; const handleReset = () => { hasUsedUndo.current = false; setDeck(initialJobs); setUndoStack([]); }; const visibleCards = deck.slice(-3); return ( <div className={`flex flex-col items-center w-full max-w-sm mx-auto ${className}`}> {/* Card Stack */} <div className="relative w-full" style={{ height: 420 }}> {isEmpty ? ( <EmptyState onReset={handleReset} /> ) : ( visibleCards.map((job, i) => { const stackIndex = visibleCards.length - 1 - i; // 0 = front (last in array) const isTop = stackIndex === 0; return ( <JobCard key={job.id} job={job} stackIndex={stackIndex} isTop={isTop} onSwipe={handleSwipe} dragX={isTop ? dragX : 0} setDragX={setDragX} /> ); }) )} </div> {/* Progress */} {!isEmpty && ( <p className="text-xs text-gray-400 mt-3 mb-1"> {deck.length} of {initialJobs.length} remaining </p> )} {/* Action buttons */} <ActionButtons onSkip={() => imperativeSwipe("left")} onLike={() => imperativeSwipe("right")} onUndo={handleUndo} canUndo={undoStack.length > 0 && !hasUsedUndo.current} isEmpty={isEmpty} /> </div> ); } // ─── Demo / Usage ───────────────────────────────────────────────────────────── const DEMO_JOBS: JobListing[] = [ { id: "1", title: "Senior Frontend Engineer", company: "Vercel", location: "San Francisco, CA", salaryRange: "$160k – $200k", remote: true, logoUrl: "https://avatars.githubusercontent.com/u/14985020", }, { id: "2", title: "React Developer", company: "Linear", location: "New York, NY", salaryRange: "$130k – $165k", remote: true, logoUrl: "https://avatars.githubusercontent.com/u/56705483", }, { id: "3", title: "UI Engineer", company: "Figma", location: "San Francisco, CA", salaryRange: "$145k – $185k", remote: false, logoUrl: "https://avatars.githubusercontent.com/u/1905596", }, { id: "4", title: "Staff Engineer, Design Systems", company: "Shopify", location: "Toronto, ON", salaryRange: "$150k – $190k", remote: true, logoUrl: "https://avatars.githubusercontent.com/u/8085", }, { id: "5", title: "Frontend Architect", company: "Stripe", location: "Remote", salaryRange: "$175k – $220k", remote: true, logoUrl: "https://avatars.githubusercontent.com/u/856813", }, ]; export default function App() { return ( <div className="min-h-screen bg-gray-50 flex flex-col items-center justify-center p-6"> <h1 className="text-2xl font-bold text-gray-900 mb-2">Job Stack</h1> <p className="text-sm text-gray-500 mb-8">Swipe right to apply · left to skip</p> <JobCardStack jobs={DEMO_JOBS} /> </div> ); }
🌀 Claude

Tinderstyle Swipe Card Stack React

Add to Cart
Instant accessInstant access
Usage rightsCommercial use
Money-back guaranteeMoney‑back
By purchasing this prompt, you agree to our terms of service
CLAUDE-4-7-OPUS
Tested icon
Guide icon
4 examples icon
Free credits icon
Generate a complete, production-ready Tinder-style swipeable card stack in React + TypeScript — drag-to-like/skip cards with @react-spring/web physics, fly-off animations, LIKE/NOPE stamps, undo, haptics, and empty-state reset. Fill in your item type, fields, styling, and backend actions — get the full component, ready to drop in. Built from a real production implementation, not generic boilerplate.
...more
Added 4 days ago
Report
Browse Marketplace