尚未检测到更新。GitHub 发布版本后会显示在这里。
个人 GitHub 账号下公开仓库的所有者可以登录并验证所有权,自行管理介绍,无需管理员批准。
登录并生成验证文件,在 30 分钟内将其提交到此仓库的默认分支。其他分支或分叉仓库中的文件无效。然后在下方检查文件,即可无需管理员批准直接管理项目。请保留文件以维持管理权限。
如需登录,将前往登录页面。登录后请回到此处,再次点击所选的验证按钮。
在项目文件夹中运行 /nomorevibe verify,即可验证所有权并自行管理介绍。
一分钟完成验证正在加载完整 README。 以下为已保存的摘录。
By Tensai Tech Inc. — peer-to-peer travel buddy matching, starting in
Shenzhen. International travelers find a local who shares their interests;
the local shows them around.
Repo slug remains shenzhen-buddies (lowercase, hyphenated) for npm/URL
conventions, but anywhere user-facing the product is "Shenzhen Buddies".
Stack: Next.js 16 (App Router), TypeScript, Tailwind v4, Supabase
(Auth + Postgres + Storage + Realtime), Stripe (subscriptions), deployed on
Vercel.查看完整 README ↗By Tensai Tech Inc. — peer-to-peer travel buddy matching, starting in Shenzhen. International travelers find a local who shares their interests; the local shows them around.
Repo slug remains
shenzhen-buddies(lowercase, hyphenated) for npm/URL conventions, but anywhere user-facing the product is "Shenzhen Buddies".
Stack: Next.js 16 (App Router), TypeScript, Tailwind v4, Supabase (Auth + Postgres + Storage + Realtime), Stripe (subscriptions), deployed on Vercel.
/u/[id]/pricing)Supabase project — create one (or use an existing one) and copy from Settings → API:
NEXT_PUBLIC_SUPABASE_URLanon public key → NEXT_PUBLIC_SUPABASE_ANON_KEYservice_role key → SUPABASE_SERVICE_ROLE_KEY (server-only secret;
used by the Stripe webhook to write rows that bypass RLS)Environment variables
cp .env.local.example .env.local
# paste values from Supabase + Stripe into .env.local
Database schema — open the Supabase SQL editor and run each migration in order:
supabase/migrations/0001_init.sql — profiles + RLSsupabase/migrations/0002_avatars.sql — avatar_path column + public
avatars storage bucketsupabase/migrations/0003_chat.sql — conversations, messages,
RLS, get_or_create_conversation RPC, Realtime publicationsupabase/migrations/0004_reviews.sql — reviews + RLS (insert
requires prior message history)supabase/migrations/0005_notifications.sql — per-user last_read_at
on conversations + mark_conversation_read RPCsupabase/migrations/0006_subscriptions.sql — subscriptions mirror
of Stripe state (only needed if you wire up Stripe)supabase/migrations/0007_match_role_constraint.sql — guide↔tourist
constraint on conversationssupabase/migrations/0008_bookings.sql — availability_windows +
bookings for the single-guide beta (originally one booking per day;
0018_booking_gap.sql relaxes that to any number of bookings at least
two hours apart, enforced by an exclusion constraint)supabase/migrations/0009_booking_payments.sql — adds Stripe payment
columns + pending_payment hold state to bookingssupabase/migrations/0019_one_booking_at_a_time.sql (0015 adds PayPal
columns — payment_provider, paypal_order_id, paypal_capture_id;
0017 widens the tour length for the free pilot; 0018 replaces the
one-booking-per-day lock with the two-hour gap rule; 0019 adds the
one-live-booking-per-tourist trigger)Auth settings (Supabase dashboard → Authentication → URL Configuration)
http://localhost:3000 (plus your Vercel URL once deployed)http://localhost:3000/auth/confirm (plus the
production equivalent).Stripe — powers premium subscriptions (/pricing) and one-time
booking payments (/guide). Leave STRIPE_SECRET_KEY blank during pilot to
disable both: bookings then skip checkout and go straight to admin review.
price_...).sk_test_...).<your-site-url>/api/stripe/webhook
listening for:
checkout.session.completed — confirms a paid booking / subscriptioncheckout.session.expired — frees the held day if a booking checkout
is abandonedcustomer.subscription.created/updated/deleted — subscription sync
Copy its signing secret (whsec_...)..env.local: STRIPE_SECRET_KEY, STRIPE_WEBHOOK_SECRET,
NEXT_PUBLIC_STRIPE_PRICE_ID (price ID only needed for subscriptions).FREE_TOURS in src/lib/booking.ts is on, so
tours are free and come in fixed lengths (FREE_TOUR_LENGTHS, 2 or 3
hours) with no payment step; flip it to false to restore paid bookings.
Tips (/guide/review/[id]) and donations (/donate) take card via
Stripe (src/lib/support.ts) and PayPal when the PayPal vars below
are set.src/lib/booking.ts (HOURLY_RATE_CENTS, CURRENCY). Declining a paid
booking auto-refunds.PayPal (optional second payment method for bookings and donations)
/guide/pay/[id]) offering card (Stripe) and, if configured,
PayPal. Leave the PayPal vars blank to show card only./donate and the post-tour tip step offer the same pair. PayPal
donations go through /api/paypal/create-donation + capture-donation
(no sign-in needed) and tips through /api/paypal/create-tip +
capture-tip (signed-in tourist, own finished tour — the rule in
src/lib/tips.ts). Nothing is written to the database — the PayPal
dashboard is the ledger — and the admins (and, for tips, the guide) get
the same emails the Stripe webhook sends for card payments..env.local: PAYPAL_ENV (sandbox or live),
PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, and
NEXT_PUBLIC_PAYPAL_CLIENT_ID (same value as PAYPAL_CLIENT_ID).PAYPAL_ENV must match the credentials pasted — sandbox keys against
sandbox, live keys against live, or PayPal returns 401 invalid_client. Test with a Sandbox buyer account (Developer dashboard →
Testing Tools → Sandbox Accounts).Single-guide beta mode (optional)
OFFICIAL_GUIDE_ID to that account's auth user id (Supabase
dashboard → Authentication → Users). While it is set, /browse
redirects to /guide, where tourists book that guide (5–15 hour slots)
instead of matching. Unset it to restore the matching experience.ADMIN_EMAILS (comma-separated) — those accounts get an Admin
nav link to /admin to publish availability and approve/decline
requests.GMAIL_USER + GMAIL_APP_PASSWORD (+ optional EMAIL_FROM) for
booking emails (new request → admins; approve/decline → tourist). On the
Gmail account, turn on 2-Step Verification and generate an App
password (Google Account → Security → App passwords); paste it into
GMAIL_APP_PASSWORD. Gmail sends the mail itself, so the @gmail.com
From reaches inboxes without owning a domain. Leave blank locally and
emails are logged to the server console instead.Run
npm run dev
Open http://localhost:3000.
NEXT_PUBLIC_SUPABASE_URL, NEXT_PUBLIC_SUPABASE_ANON_KEY,
SUPABASE_SERVICE_ROLE_KEY, and the three STRIPE_* /
NEXT_PUBLIC_STRIPE_PRICE_ID if you're enabling subscriptions. For the
single-guide beta also add OFFICIAL_GUIDE_ID, ADMIN_EMAILS,
GMAIL_USER, GMAIL_APP_PASSWORD, and EMAIL_FROM. To enable PayPal add
PAYPAL_ENV (live), PAYPAL_CLIENT_ID, PAYPAL_CLIENT_SECRET, and
NEXT_PUBLIC_PAYPAL_CLIENT_ID.Instagram is currently the only live traffic source, and an untagged click from the bio is indistinguishable from direct traffic in GA4. Every bio link, story link and swipe-up must carry:
?utm_source=instagram&utm_medium=social&utm_campaign=<slug>
<slug> names the campaign or post series, lowercase and hyphenated
(hidden-gems, street-food-aug). Keep one slug per series so sessions group.
<NEXT_PUBLIC_SITE_URL>/guide?utm_source=instagram&utm_medium=social&utm_campaign=hidden-gems
Point campaign links at /guide (the page that closes a booking) or / — both
are public, so a logged-out follower sees the guide preview rather than a login
wall. To fire a custom GA4 event from a client component, use the track()
helper in src/lib/analytics.ts; it no-ops safely when NEXT_PUBLIC_GA_ID is
unset and must never be passed personal data.
/gallery reads from src/content/gallery.ts — a typed module, not a database
table. Adding photos is three steps and no migration.
1. Drop the originals into a topic folder under marketing/assets/, named
per that folder's README (huaqiangbei-aisle-night-01.jpg). The originals are
gitignored — camera files carry EXIF GPS and full-resolution faces, and git
history is permanent. Before photographing any identifiable person, read
marketing/assets/CONSENT.md: consent is a logged ledger entry, and the
filename carries only the opaque consent ID (…-SBC-2026-08-001-consented.jpg),
never a name.
2. Convert them. This resizes the longest edge to 1600px, writes WebP into
public/gallery/ under the 300 KB budget, and prints a paste-ready block with
the measured width/height for each file:
node scripts/img.mjs marketing/assets/huaqiangbei
3. Paste each block into the galleryItems array in
src/content/gallery.ts and fill in alt, title, location, themes, and
optionally caption / featured.
The build is the validation layer. assertGalleryValid() runs when the module
loads, so npm run build fails — rather than production — on a duplicate id,
an empty alt, a missing width/height, or a people: 'consented' item whose
consentRef is not a well-formed consent ID (SBC-YYYY-MM-NNN).
What that does not cover: people is one flag for the whole frame, so a
consented subject photographed in front of recognisable bystanders passes the
build and still contains unconsented faces. Composition is the control there —
"shoot places, not faces", per CONSENT.md. Only fields listed in
PublicGalleryItem are sent to the browser, so consentRef and credit stay
server-side.
A location only gets a filter chip once it has 4 items, so a half-shot neighbourhood stays invisible instead of rendering a chip that leads to two photos. With zero items the page renders its "first batch is being shot" state.
Video is the exception to "commit it": MP4s go to the Supabase Storage public
gallery bucket (≤ 8 MB, ≤ 20 s) and the item carries the absolute URL plus a
poster WebP that does live in public/gallery/.
src/
app/
page.tsx landing
login/ email + password login
signup/ email + password signup
profile/ protected: create / edit profile
browse/ public: discover guides (messaging gated)
guide/ public: guide preview + booking (auth at book)
u/[id]/ public profile detail + review form
messages/ conversations list
messages/[id]/ thread view (client realtime)
pricing/ subscription page (Stripe Checkout)
gallery/ public: real photos, ?loc= server-filtered
auth/confirm/route.ts email-confirmation callback
api/stripe/webhook/route.ts Stripe webhook handler
layout.tsx root layout (header + footer)
components/
Avatar.tsx image / initials fallback
SubmitButton.tsx useFormStatus pending state
StarRating.tsx display + input variants
content/
gallery.ts typed photo library + build-time validation
lib/
avatars.ts public-URL helper
matching.ts score function for browse
stripe.ts Stripe SDK + entitlement helper
supabase/
client.ts browser client
server.ts server client (cookies-bound)
admin.ts service-role client (webhook only)
proxy.ts session refresh in src/proxy.ts
proxy.ts runs on every request
scripts/
img.mjs founder photos -> WebP in public/gallery/
supabase/
migrations/ run in order in the SQL editor
Note: Next.js 16 renamed middleware to proxy. The file is
src/proxy.ts, exporting aproxyfunction. Same purpose as the old middleware. Production build must use webpack ("build": "next build --webpack"in package.json) — the default Turbopack output skips build-traces that Vercel's adapter needs to register serverless functions.