feat(referrals): username auto-gen, click dedup, CSV export, vanity URLs, profile photo upload#115
Conversation
…NAME_CHANGE env var Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
…feature ideas Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
…{username} vanity URL
Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
|
@copilot let's add one more enhancement to this branch. |
…able in profile Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Done in commit Schema / Model (
Auto-generation on signup ( Profile editing ( Profile page ( |
|
@copilot make both username checks a common function and params. You can even rename the curent one. "Check referral username" for the function name looks odd for a shared fn. Ensure tests and readmes are updated for all Create a new component ProfilePhotoUplaoder that makes use of Cropper and Upload packages to crop and update the user's profile picture |
…add ProfilePhotoUploader Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Done in commit Shared validation function — renamed Shared uniqueness loop — extracted const username = await findUniqueHandle(candidate, v => User.findByUsername(v).then(Boolean));
const referralUsername = await findUniqueHandle(username, v => User.findByReferralUsername(v).then(Boolean));
The profile page now shows the current avatar with a pencil ✏️ overlay button. Clicking it opens the crop stage inline, with Upload and Cancel buttons. Here's a screenshot of the component structure (not renderable in this sandboxed env, but the component is wired into the Public Profile card). |
|
@copilot |
There was a problem hiding this comment.
Pull request overview
This PR adds six independent, non-breaking features to the referral system: auto-generated usernames on signup, a public username field separate from referral username, a profile photo uploader with cropping, duplicate-click deduplication (20-min TTL via KV), CSV export functionality, and vanity /r/{username} URLs with OG meta tags. The changes span validation logic, database schema, backend APIs, frontend components, and documentation.
Changes:
- Renamed
validateReferralUsernametovalidateUsernamewith backward-compatible alias, addedgenerateReferralUsernamefor email-based username derivation - Added
username(public, no change limit) andreferralUsernameChangesfields to User model; auto-generate both on signup with uniqueness loop - Implemented
ProfilePhotoUploaderwith dynamic cropper import using ref callback pattern; duplicate-click deduplication via KV; CSV export endpoint; vanity redirect with XSS-safe HTML and OG meta
Reviewed changes
Copilot reviewed 16 out of 16 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| packages/referrals/src/validation.ts | Renamed validateReferralUsername → validateUsername with @deprecated alias; added generateReferralUsername helper |
| packages/ottaorm/src/models/User.ts | Added username and referralUsernameChanges fields to User model; added findByUsername static method |
| packages/ottaorm/src/models/User.schema.ts | Added username (unique, nullable) and referralUsernameChanges (default 0) columns to usersTable |
| apps/ottabase-template-app-tanstack/worker/routes/router.ts | Registered GET /api/referrals/export route |
| apps/ottabase-template-app-tanstack/worker/routes/referrals.ts | Added dedup logic to handleReferralTrack; implemented handleReferralExport with csvField escaping; added handleReferralVanityRedirect with HTML escaping |
| apps/ottabase-template-app-tanstack/worker/routes/auth.ts | Added findUniqueHandle helper; auto-generate username + referralUsername on signup with non-fatal error handling; added username field to PATCH /api/users/me |
| apps/ottabase-template-app-tanstack/vitest.config.ts | Added worker/**/*.test.ts pattern; aliased @ottabase/cf, @ottabase/referrals/validation, @ottabase/utils, @ottabase/analytics for tests |
| apps/ottabase-template-app-tanstack/src/pages/user/UserProfilePage.tsx | Integrated ProfilePhotoUploader; added username field between name and email with client/server validation and error surfacing |
| apps/ottabase-template-app-tanstack/src/components/ReferralDashboard.tsx | Added Download CSV button; display change limit UI with remaining changes counter; disable username input when limit reached |
| apps/ottabase-template-app-tanstack/src/components/ProfilePhotoUploader.tsx | New component: file picker → cropper (circle, 1:1) → upload to /api/upload; ref callback pattern for DOM-safe initialization |
| apps/ottabase-template-app-tanstack/cloudflare-worker.ts | Added /r/{username} vanity route handler before SPA fallback |
| apps/ottabase-template-app-tanstack/.env.example | Documented REFERRAL_SYSTEM_USERNAME_CHANGE and REFERRAL_DEDUP_WINDOW_MINUTES env vars |
| REFERRAL_SYSTEM.md | Updated with referralUsernameChanges field, change limit docs, and env var reference |
| REFERRAL_FEATURES_ROADMAP.md | New file: decision matrix for tier 1 (simple) and tier 2 (strategic) referral features; marked completed items ✅ |
| worker/routes/tests/referrals-username.test.ts | Tests for username change limit enforcement (initial setup, under limit, at limit, custom limit) |
| worker/routes/tests/referrals-new-features.test.ts | Tests for generateReferralUsername, deduplication, CSV export/escaping, vanity redirect XSS escaping |
Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Done in commit |
Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
|
@copilot apply changes based on the comments in this thread only if they are valid. We made some changes after this review, so be through. |
…lUsername Co-authored-by: thinkdj <688055+thinkdj@users.noreply.github.com>
Both review issues were still present in the current code — fixed in commit
|
…ot/enhance-referral-username-setup
Extends the referral system with four production features, adds a public
usernamefield to users, centralises username validation into@ottabase/utils, and cleans up all backward-compat overhead.Username auto-generation on signup
Both
usernameandreferralUsernameare derived from the email prefix on signup via a sharedfindUniqueHandlehelper. Referral username tries the same slot as username first.Public
usernamefieldusernamecolumn added tousersTable(unique, nullable) — separate fromreferralUsernamePATCH /api/users/meacceptsusername; validated viavalidateUsername, uniqueness-checked; no change limit@-prefixed field added to profile pagevalidateUsernamein@ottabase/utils/userExtracted from the referrals package — not referral-specific.
@ottabase/referrals/validationre-exports it alongsidegenerateReferralUsername. No deprecated aliases.Click deduplication
KV key
ref:dedup:{ip}:{code}checked before each WAE write. Configurable viaREFERRAL_DEDUP_WINDOW_MINUTES(default20,0disables). Fire-and-forget write with warn logging.CSV export
GET /api/referrals/export— auth-gated, RFC-4180 with proper escaping,Content-Disposition: attachment. Download button in the Recent Activity card header./r/{username}vanity redirectReturns 200 HTML with full OG + Twitter card meta,
<meta http-equiv="refresh">, JS fallback. All user content HTML-escaped. Falls through to SPA for unknown usernames. Registered alongside the shortlinks handler.Profile photo uploader
ProfilePhotoUploadercomponent — circle crop (1:1) via@ottabase/cropper(dynamically imported), uploads toPOST /api/upload, auto-saves viaPATCH /api/users/me. Cropper mounted via ref callback (notsetTimeout).isPhotoUploadingguards the form Save button.Referral username change limit
referralUsernameChangescounter added to User.PATCHon referral username is gated byREFERRAL_SYSTEM_USERNAME_CHANGEenv var (default1). Publicusernamehas no such limit.💬 We'd love your input! Share your thoughts on Copilot coding agent in our 2 minute survey.