Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions apps/web/app/api/bounties/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export async function POST(req: NextRequest) {
const did = await getSessionDid();
if (!did) return NextResponse.json({ error: 'Authentication required' }, { status: 401 });

const { title, description, reward_usd, store_id, store_name } = await req.json();
const { title, description, url, reward_usd, store_id, store_name } = await req.json();

if (!title?.trim()) return NextResponse.json({ error: 'Title is required' }, { status: 400 });
const reward = parseFloat(reward_usd);
Expand All @@ -43,14 +43,15 @@ export async function POST(req: NextRequest) {

// Insert bounty as 'open' with a non-sequential public id for URLs.
await db.sql`
INSERT INTO bounties (public_id, creator_did, store_id, store_name, title, description, reward_usd, status)
INSERT INTO bounties (public_id, creator_did, store_id, store_name, title, description, url, reward_usd, status)
VALUES (
${publicId},
${did},
${store_id ?? null},
${store_name?.trim() ?? null},
${title.trim()},
${description?.trim() ?? null},
${url?.trim() || null},
${reward},
'open'
)
Expand Down
7 changes: 2 additions & 5 deletions apps/web/app/api/coupons/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,12 +95,9 @@ export async function POST(req: NextRequest) {
image_url,
} = body;

if (!url) {
return NextResponse.json({ error: 'url is required' }, { status: 400 });
}
if (!title) {
return NextResponse.json(
{ error: 'title is required (scrape the URL first)' },
{ error: 'title is required' },
{ status: 400 }
);
}
Expand All @@ -125,7 +122,7 @@ export async function POST(req: NextRequest) {
)
VALUES (
${storeId}, ${code?.trim() || null}, ${title}, ${description ?? null}, ${discount},
${type}, ${value}, ${expiry_date || null}, ${url}, ${image_url ?? null}
${type}, ${value}, ${expiry_date || null}, ${url?.trim() || null}, ${image_url ?? null}
)
`;
return NextResponse.json({ success: true }, { status: 201 });
Expand Down
13 changes: 13 additions & 0 deletions apps/web/app/bounties/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ interface Bounty {
public_id: string;
title: string;
description: string | null;
url: string | null;
reward_usd: number;
status: string;
store_id: number | null;
Expand Down Expand Up @@ -103,6 +104,18 @@ export default async function BountyPage({ params }: { params: Promise<{ id: str
<p className="text-gray-600 leading-relaxed border-t border-gray-100 pt-4">{bounty.description}</p>
)}

{/* Reference link */}
{bounty.url && (
<a
href={bounty.url}
target="_blank"
rel="noopener noreferrer"
className="text-sm text-orange-500 hover:underline break-all"
>
{bounty.url} ↗
</a>
)}

{/* Claimed coupon */}
{bounty.coupon_code && (
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
Expand Down
9 changes: 9 additions & 0 deletions apps/web/app/bounties/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default function NewBountyPage() {
const [form, setForm] = useState({
title: '',
description: '',
url: '',
reward_usd: '0.10',
store_id: '',
store_name: '',
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function NewBountyPage() {
body: JSON.stringify({
title: form.title.trim(),
description: form.description.trim() || null,
url: form.url.trim() || null,
reward_usd: reward,
store_id: form.store_id ? parseInt(form.store_id) : null,
store_name: !form.store_id ? form.store_name.trim() : null,
Expand Down Expand Up @@ -138,6 +140,13 @@ export default function NewBountyPage() {
rows={3}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-orange-400 resize-none"
/>
<input
type="url"
placeholder="Link (optional) — e.g. the product or store page"
value={form.url}
onChange={e => set('url', e.target.value)}
className="w-full border border-gray-200 rounded-lg px-3 py-2 text-sm text-gray-900 focus:outline-none focus:ring-2 focus:ring-orange-400"
/>
<div>
<label className="text-xs text-gray-500 mb-1 block">Reward amount (USD, min $0.10) *</label>
<div className="flex items-center border border-gray-200 rounded-lg overflow-hidden focus-within:ring-2 focus-within:ring-orange-400">
Expand Down
22 changes: 13 additions & 9 deletions apps/web/app/submit/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,8 @@ export default function SubmitPage() {
e.preventDefault();
setError('');

if (!url.trim()) {
setError('A product URL is required.');
return;
}
if (!scraped || !scraped.title.trim()) {
setError('Fetch the listing details first (or add a title).');
setError('Add a title (fetch it from a link, or enter details manually).');
return;
}

Expand All @@ -94,7 +90,7 @@ export default function SubmitPage() {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
url: url.trim(),
url: url.trim() || null,
code: code.trim() || null,
discount_type: discountValue.trim() ? discountType : null,
discount_value: discountValue.trim() || null,
Expand Down Expand Up @@ -137,21 +133,20 @@ export default function SubmitPage() {
<div className="max-w-lg mx-auto">
<h1 className="text-3xl font-black text-gray-900 mb-2">Submit a Coupon</h1>
<p className="text-gray-500 mb-8 text-sm">
Paste the product link and your coupon codewe&apos;ll pull the listing details for you.
Paste a product link to auto-fill the detailsor enter them manually. The link is optional.
</p>

<form onSubmit={handleSubmit} className="flex flex-col gap-6">
{/* Product URL + fetch */}
<div className="bg-white border border-gray-200 rounded-2xl p-6 flex flex-col gap-4">
<h2 className="font-bold text-gray-800">Product link</h2>
<h2 className="font-bold text-gray-800">Product link <span className="font-normal text-gray-400 text-sm">(optional)</span></h2>
<div className="flex gap-2">
<input
type="url"
placeholder="https://store.com/product/…"
value={url}
onChange={(e) => setUrl(e.target.value)}
className={`flex-1 ${inputClass}`}
required
/>
<button
type="button"
Expand All @@ -165,6 +160,15 @@ export default function SubmitPage() {
<p className="text-xs text-gray-400">
We use AI to read the title, store, and image from the page. You can edit anything below.
</p>
{!scraped && (
<button
type="button"
onClick={() => setScraped({ ...EMPTY })}
className="self-start text-sm text-orange-500 hover:underline"
>
or enter details manually →
</button>
)}
</div>

{/* AI-scraped, editable preview */}
Expand Down
4 changes: 4 additions & 0 deletions apps/web/scripts/migrate.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ await db.sql`
await addColumn(() => db.sql`ALTER TABLE coupons ADD COLUMN discount_type TEXT`);
await addColumn(() => db.sql`ALTER TABLE coupons ADD COLUMN discount_value REAL`);
await addColumn(() => db.sql`ALTER TABLE coupons ADD COLUMN image_url TEXT`);
await addColumn(() => db.sql`ALTER TABLE coupons ADD COLUMN url TEXT`);
console.log(' coupons');

await db.sql`
Expand Down Expand Up @@ -133,6 +134,7 @@ await db.sql`
store_name TEXT,
title TEXT NOT NULL,
description TEXT,
url TEXT,
reward_usd REAL NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
payment_id TEXT,
Expand All @@ -144,6 +146,8 @@ await db.sql`
`;
// public_id: non-sequential URL identifier (added after initial release).
await addColumn(() => db.sql`ALTER TABLE bounties ADD COLUMN public_id TEXT`);
// url: optional reference link for the bounty (added after initial release).
await addColumn(() => db.sql`ALTER TABLE bounties ADD COLUMN url TEXT`);
// Backfill any rows missing a public_id (existing bounties created pre-migration).
const needIds = await db.sql`SELECT id FROM bounties WHERE public_id IS NULL OR public_id = ''`;
for (const row of needIds) {
Expand Down
Loading