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>
);
}
By purchasing this prompt, you agree to our terms of service
CLAUDE-4-7-OPUS
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
