关于 waitlist
waitlist 是一项为现有项目搭建候补名单功能的技能,先检查项目使用的技术栈,再生成相应实现。内容涵盖报名表、数据存储、确认邮件、推荐追踪和管理视图。
主要功能
- 01检测现有项目技术栈
- 02创建报名接口与表单
- 03适配数据库存储
- 04发送确认邮件
- 05支持推荐追踪与管理视图
适用场景
- 01为产品上线收集候补用户
- 02将候补名单接入现有数据库
- 03向报名者发送确认邮件
- 04查看报名记录与推荐数据
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ tushaarmehtaa/ tushar-skills --skill waitlist安装后在 Claude Code 中输入/waitlist确认已加载。
详细介绍
构建一个完整的候补名单功能——收集邮箱、存储数据、发送确认邮件并提供管理视图。先读取项目,再接入现有技术栈。
第 1 阶段:检测项目
cat package.json | grep -E "next|supabase|prisma|drizzle|resend"
检查以下内容:
- 数据库:Supabase / Prisma / Drizzle / 原始 Postgres?
- 邮件:是否已安装 Resend?
- 身份验证:是否有身份验证提供商?(管理视图需要)
- 现有候补名单表:在架构文件中搜索
waitlist
第 2 阶段:询问用户
I'll scaffold a waitlist for [product name].
Quick decisions:
1. What info do you want to collect?
a) Email only (default — highest conversion)
b) Email + name
c) Email + name + "what are you building?" (optional field)
2. Referral tracking?
a) No — simple list
b) Yes — each signup gets a unique referral link, referred count tracked
3. Confirmation email?
a) Yes — send a "you're on the list" email via Resend (default)
b) No
4. Do you have Resend and a verified sending domain? (yes / need to set up)
第 3 阶段:数据库架构
根据检测到的数据库进行调整。创建一个 waitlist 表:
Supabase / 原始 SQL:
create table public.waitlist (
id uuid primary key default gen_random_uuid(),
email text not null unique,
name text,
note text, -- optional "what are you building?" field
referral_code text unique, -- their shareable code (if referral enabled)
referred_by text references public.waitlist(referral_code), -- who sent them
referral_count integer default 0, -- how many they've referred
position integer, -- queue position (set by trigger)
confirmed_at timestamptz, -- null = unconfirmed
created_at timestamptz default now()
);
-- Auto-increment position on insert
create or replace function set_waitlist_position()
returns trigger as $$
begin
new.position = (select coalesce(max(position), 0) + 1 from public.waitlist);
return new;
end;
$$ language plpgsql;
create trigger waitlist_position_trigger
before insert on public.waitlist
for each row execute function set_waitlist_position();
如果不需要推荐跟踪,请移除 referral_code、referred_by、referral_count。
Prisma:
model Waitlist {
id String @id @default(cuid())
email String @unique
name String?
note String?
referralCode String? @unique
referredBy String?
referralCount Int @default(0)
position Int
confirmedAt DateTime?
createdAt DateTime @default(now())
}
第 4 阶段:注册 API 路由
// app/api/waitlist/route.ts
import { NextResponse } from 'next/server';
export async function POST(req: Request) {
const { email, name, note, referredBy } = await req.json();
// Validate email
if (!email || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
return NextResponse.json({ error: 'Valid email required' }, { status: 400 });
}
// Normalize
const normalizedEmail = email.toLowerCase().trim();
// Check for duplicate
const existing = await db.query.waitlist.findFirst({
where: eq(waitlist.email, normalizedEmail),
});
if (existing) {
// Return success — don't tell spammers which emails are registered
return NextResponse.json({
success: true,
position: existing.position,
alreadyRegistered: true,
});
}
// Generate referral code if enabled
const referralCode = generateReferralCode(normalizedEmail); // e.g., first 8 chars of email hash
// Insert
const entry = await db.insert(waitlistTable).values({
email: normalizedEmail,
name: name?.trim() || null,
note: note?.trim() || null,
referralCode,
referredBy: referredBy || null,
}).returning();
// Increment referrer's count
if (referredBy) {
await db.update(waitlistTable)
.set({ referralCount: sql`referral_count + 1` })
.where(eq(waitlistTable.referralCode, referredBy));
}
// Send confirmation email (if Resend is set up)
if (process.env.RESEND_API_KEY) {
await sendWaitlistConfirmation({
email: normalizedEmail,
name: name || 'there',
position: entry[0].position,
referralCode,
});
}
return NextResponse.json({
success: true,
position: entry[0].position,
referralCode,
});
}
function generateReferralCode(email: string): string {
const { createHash } = require('crypto');
return createHash('sha256').update(email).digest('hex').slice(0, 8);
}
第 5 阶段:确认邮件
// lib/emails/waitlist-confirmation.ts
import { Resend } from 'resend';
const resend = new Resend(process.env.RESEND_API_KEY);
export async function sendWaitlistConfirmation({
email, name, position, referralCode,
}: { email: string; name: string; position: number; referralCode?: string }) {
const referralUrl = referralCode
? `${process.env.NEXT_PUBLIC_APP_URL}/?ref=${referralCode}`
: null;
await resend.emails.send({
from: `[Product Name] <[email protected]>`,
to: email,
subject: `you're #${position} on the list`,
html: `
<div style="font-family: sans-serif; max-width: 520px; margin: 0 auto; padding: 40px 20px; color: #111;">
<p>Hey ${name},</p>
<p>You're <strong>#${position}</strong> on the waitlist. We'll reach out when you're up.</p>
${referralUrl ? `
<p style="margin-top: 24px;">
Move up the list — share your link:<br>
<a href="${referralUrl}" style="color: #000; font-weight: bold;">${referralUrl}</a>
</p>
` : ''}
<p style="color: #666; font-size: 13px; margin-top: 40px;">— [Your Name]</p>
</div>
`,
});
}
第 6 阶段:注册表单组件
// components/waitlist-form.tsx
'use client';
import { useState } from 'react';
export function WaitlistForm({ referralCode }: { referralCode?: string }) {
const [email, setEmail] = useState('');
const [state, setState] = useState<'idle' | 'loading' | 'success' | 'error'>('idle');
const [position, setPosition] = useState<number | null>(null);
const [userReferralCode, setUserReferralCode] = useState<string | null>(null);
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
setState('loading');
const res = await fetch('/api/waitlist', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, referredBy: referralCode }),
});
const data = await res.json();
if (data.success) {
setState('success');
setPosition(data.position);
setUserReferralCode(data.referralCode);
} else {
setState('error');
}
}
if (state === 'success') {
return (
<div>
<p>you're #{position} on the list.</p>
{userReferralCode && (
<p>
share your link to move up:{' '}
<code>{`${window.location.origin}/?ref=${userReferralCode}`}</code>
</p>
)}
</div>
);
}
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="[email protected]"
required
disabled={state === 'loading'}
/>
<button type="submit" disabled={state === 'loading'}>
{state === 'loading' ? 'joining...' : 'join waitlist'}
</button>
{state === 'error' && <p>something went wrong. try again.</p>}
</form>
);
}
在页面中从 URL 参数读取推荐码:
// app/page.tsx
export default function Page({ searchParams }: { searchParams: { ref?: string } }) {
return <WaitlistForm referralCode={searchParams.ref} />;
}
第 7 阶段:管理视图
使用需要密钥请求头的简单路由即可——候补名单管理无需完整身份验证:
// app/api/admin/waitlist/route.ts
export async function GET(req: Request) {
if (req.headers.get('x-admin-secret') !== process.env.ADMIN_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 });
}
const signups = await db.select().from(waitlistTable).orderBy(asc(waitlistTable.position));
return Response.json({
total: signups.length,
signups,
});
}
使用以下命令获取数据:curl -H "x-admin-secret: $ADMIN_SECRET" https://yoursite.com/api/admin/waitlist
验证
[ ] Duplicate email returns success with alreadyRegistered: true (no error, no leak)
[ ] Email normalized to lowercase before storage
[ ] Position increments correctly — first signup is #1
[ ] Confirmation email arrives in inbox, not spam
[ ] Referral code in URL (?ref=) is captured and stored on signup
[ ] Referrer's count increments when referred signup completes
[ ] Admin route requires x-admin-secret header
[ ] ADMIN_SECRET and RESEND_API_KEY in .env.example
[ ] Form disables input during submission
[ ] Success state shows position and referral link
评论
支持 Markdown