From 4026f7e0fdcebe88a9751ad59a4afc54d3716ba7 Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 21:30:51 -0700 Subject: [PATCH 01/59] Add database schema design principles This commit introduces a new knowledge file for database schema design principles. It covers general guidelines for schema simplicity and efficiency, as well as specific recommendations for user and referral table designs. Key points include: - Reusing existing fields when possible - Using composite primary keys where appropriate - Utilizing the 'quota' field for multiple purposes - Efficient design for referral systems These principles aim to improve database performance and maintainability. --- common/src/db/schema.knowledge.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 common/src/db/schema.knowledge.md diff --git a/common/src/db/schema.knowledge.md b/common/src/db/schema.knowledge.md new file mode 100644 index 000000000..76588b276 --- /dev/null +++ b/common/src/db/schema.knowledge.md @@ -0,0 +1,23 @@ +# Database Schema Design Principles + +## General Guidelines + +- Prefer simplicity and efficiency in schema design. +- Reuse existing fields when possible instead of creating new ones. +- Use composite primary keys where appropriate to avoid unnecessary unique identifiers. + +## Specific Fields + +### User Table + +- `quota`: This field is used to track user credits. It should be used for features like referral rewards instead of creating a separate credits field. +- `referral_code`: Store the user's referral code in the user table rather than in a separate referrals table. + +## Referrals Table Design + +When implementing a referrals system: + +- Use a composite primary key of `referrer_id` and `referred_id` instead of a separate unique identifier. +- Avoid duplicating information that can be stored in the user table (e.g., referral_code). +- Utilize the existing `quota` field in the user table for tracking and updating referral rewards. + From 8ed5f852f79d268697e3e07d3c1b0f7d04b704f1 Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 21:37:50 -0700 Subject: [PATCH 02/59] Add referral system implementation plan This commit outlines the database schema changes and implementation steps for the new referral feature. It includes: - Updates to the user table - Creation of a new referral table - Detailed implementation steps covering database, API, UI, and testing - Guidelines for using existing fields and efficient schema design The plan provides a comprehensive roadmap for developing the referral system, ensuring consistency and efficiency across the application. --- common/src/db/schema.knowledge.md | 79 +++++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 14 deletions(-) diff --git a/common/src/db/schema.knowledge.md b/common/src/db/schema.knowledge.md index 76588b276..6217b1d23 100644 --- a/common/src/db/schema.knowledge.md +++ b/common/src/db/schema.knowledge.md @@ -1,23 +1,74 @@ -# Database Schema Design Principles +# Referral Feature Implementation Plan -## General Guidelines +## Database Schema Changes -- Prefer simplicity and efficiency in schema design. -- Reuse existing fields when possible instead of creating new ones. -- Use composite primary keys where appropriate to avoid unnecessary unique identifiers. +### User Table Update -## Specific Fields +- Add `referral_code` field to the `user` table + - Type: `text` + - Unique constraint + - Default value: randomly generated UUID with a prefix of `ref-` -### User Table +### New Referral Table -- `quota`: This field is used to track user credits. It should be used for features like referral rewards instead of creating a separate credits field. -- `referral_code`: Store the user's referral code in the user table rather than in a separate referrals table. +Create a new `referral` table with the following structure: -## Referrals Table Design +- `referrer_id` (part of composite primary key, foreign key to user table) +- `referred_id` (part of composite primary key, foreign key to user table) +- `status` (e.g., 'pending', 'completed') +- `created_at` timestamp +- `completed_at` timestamp (when the referred user signs up) -When implementing a referrals system: +## Implementation Steps -- Use a composite primary key of `referrer_id` and `referred_id` instead of a separate unique identifier. -- Avoid duplicating information that can be stored in the user table (e.g., referral_code). -- Utilize the existing `quota` field in the user table for tracking and updating referral rewards. +1. Update Database Schema + - Modify `common/src/db/schema.ts` to include the new `referral` table and update the `user` table + +2. NextJS Pages + + - Create `web/src/app/referrals/page.tsx` for users to view and manage referrals + - Update `web/src/app/onboard/page.tsx` to handle referral codes during sign-up + +3. API Routes + + - Create `web/src/app/api/referrals/route.ts` for referral-related operations + - Add validation to prevent self-referrals. + - Limit each referral code to be used 5 times. + - Implement rate limiting for referral code generation in a local cache automatically clears every day. Nothing fancy needed. + +4. Backend Logic + + - Update `backend/src/websockets/websocket-action.ts` for referral code generation and validation + - Implement logic to update user quota when a referral is successful + +5. Constants + + - Add referral-related constants (e.g., quota reward amounts) to `common/src/constants.ts` + +6. Authentication Flow + + - Modify `web/src/app/api/auth/[...nextauth]/auth-options.ts` to add referral code to `redirect` URL, if it was provided. + - Ensure proper error handling for all new operations + +7. UI Components + + - displaying referral information + - sharing referral code + - regenerating referral code + - inputting referral link/code manually + +8. Testing + + - Add unit tests for new database operations and API routes + - Create integration tests for the referral flow + +9. Documentation + + - Update relevant documentation to include information about the referral system + +## Notes + +- The existing `quota` field in the `user` table will be used to manage referral rewards +- The referral system leverages the composite primary key of `referrer_id` and `referred_id` for efficiency +- The `referral_code` is stored in the `user` table for simplified lookups and management From e3dd0386dc050368bad531343ed93c4a739cf41b Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 21:44:21 -0700 Subject: [PATCH 03/59] Add referral system and update user schema This commit introduces a referral system to the application: - Add 'referral_code' field to user table - Create new 'referral' table to track referrals - Update schema.ts with new fields and table definitions The referral system will allow users to invite others and track the status of their referrals. This lays the groundwork for future features like referral bonuses or rewards. --- common/src/db/schema.ts | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/common/src/db/schema.ts b/common/src/db/schema.ts index f7d3d17e4..c08f71df5 100644 --- a/common/src/db/schema.ts +++ b/common/src/db/schema.ts @@ -8,6 +8,7 @@ import { boolean, jsonb, numeric, + uuid, } from 'drizzle-orm/pg-core' import type { AdapterAccount } from 'next-auth/adapters' @@ -28,6 +29,9 @@ export const user = pgTable('user', { next_quota_reset: timestamp('next_quota_reset', { mode: 'date' }).default( sql`now() + INTERVAL '1 month'` ), + referral_code: text('referral_code') + .unique() + .default(sql`'ref-' || gen_random_uuid()`), }) export const account = pgTable( @@ -54,6 +58,25 @@ export const account = pgTable( }) ) +export const referral = pgTable( + 'referral', + { + referrer_id: text('referrer_id') + .notNull() + .references(() => user.id), + referred_id: text('referred_id') + .notNull() + .references(() => user.id), + status: text('status').notNull().default('pending'), + created_at: timestamp('created_at', { mode: 'date' }) + .notNull() + .defaultNow(), + completed_at: timestamp('completed_at', { mode: 'date' }), + }, + (table) => ({ + pk: primaryKey({ columns: [table.referrer_id, table.referred_id] }), + }) +) export const fingerprint = pgTable('fingerprint', { id: text('id').primaryKey(), sig_hash: text('sig_hash'), From b02eb3d4c7951a0226d56295d826dab56c9f2a23 Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 22:04:17 -0700 Subject: [PATCH 04/59] Add referral system and user referral code This commit introduces a referral system with the following changes: - Create 'referral' table to track referrals between users - Add 'referral_code' column to 'user' table - Implement 'ReferralStatus' enum for tracking referral status - Update schema and types to support new referral functionality These changes lay the groundwork for implementing user referrals and tracking their status within the application. --- .../db/migrations/0004_neat_pet_avengers.sql | 29 + .../src/db/migrations/meta/0004_snapshot.json | 589 ++++++++++++++++++ common/src/db/migrations/meta/_journal.json | 7 + common/src/db/schema.ts | 10 +- common/src/types/referral.ts | 3 + 5 files changed, 637 insertions(+), 1 deletion(-) create mode 100644 common/src/db/migrations/0004_neat_pet_avengers.sql create mode 100644 common/src/db/migrations/meta/0004_snapshot.json create mode 100644 common/src/types/referral.ts diff --git a/common/src/db/migrations/0004_neat_pet_avengers.sql b/common/src/db/migrations/0004_neat_pet_avengers.sql new file mode 100644 index 000000000..6909022da --- /dev/null +++ b/common/src/db/migrations/0004_neat_pet_avengers.sql @@ -0,0 +1,29 @@ +DO $$ BEGIN + CREATE TYPE "public"."referral_status" AS ENUM('pending', 'completed'); +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "referral" ( + "referrer_id" text NOT NULL, + "referred_id" text NOT NULL, + "status" "referral_status" DEFAULT 'pending' NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "completed_at" timestamp, + CONSTRAINT "referral_referrer_id_referred_id_pk" PRIMARY KEY("referrer_id","referred_id") +); +--> statement-breakpoint +ALTER TABLE "user" ADD COLUMN "referral_code" text DEFAULT 'ref-' || gen_random_uuid();--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "referral" ADD CONSTRAINT "referral_referrer_id_user_id_fk" FOREIGN KEY ("referrer_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "referral" ADD CONSTRAINT "referral_referred_id_user_id_fk" FOREIGN KEY ("referred_id") REFERENCES "public"."user"("id") ON DELETE no action ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +ALTER TABLE "user" ADD CONSTRAINT "user_referral_code_unique" UNIQUE("referral_code"); \ No newline at end of file diff --git a/common/src/db/migrations/meta/0004_snapshot.json b/common/src/db/migrations/meta/0004_snapshot.json new file mode 100644 index 000000000..4b1667141 --- /dev/null +++ b/common/src/db/migrations/meta/0004_snapshot.json @@ -0,0 +1,589 @@ +{ + "id": "14fe47ea-d311-4f6a-b602-37794097cf07", + "prevId": "f13073d6-b3ee-4808-80f7-cde01d1875df", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "providerAccountId": { + "name": "providerAccountId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_state": { + "name": "session_state", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "account_userId_user_id_fk": { + "name": "account_userId_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "account_provider_providerAccountId_pk": { + "name": "account_provider_providerAccountId_pk", + "columns": [ + "provider", + "providerAccountId" + ] + } + }, + "uniqueConstraints": {} + }, + "public.fingerprint": { + "name": "fingerprint", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "sig_hash": { + "name": "sig_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quota_exceeded": { + "name": "quota_exceeded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.message": { + "name": "message", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_request_id": { + "name": "client_request_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "request": { + "name": "request", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "last_message": { + "name": "last_message", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "\"message\".\"request\" -> -1", + "type": "stored" + } + }, + "response": { + "name": "response", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_creation_input_tokens": { + "name": "cache_creation_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cache_read_input_tokens": { + "name": "cache_read_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "cost": { + "name": "cost", + "type": "numeric(100, 20)", + "primaryKey": false, + "notNull": true + }, + "credits": { + "name": "credits", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "message_user_id_user_id_fk": { + "name": "message_user_id_user_id_fk", + "tableFrom": "message", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "message_fingerprint_id_fingerprint_id_fk": { + "name": "message_fingerprint_id_fingerprint_id_fk", + "tableFrom": "message", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.referral": { + "name": "referral", + "schema": "", + "columns": { + "referrer_id": { + "name": "referrer_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "referred_id": { + "name": "referred_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "referral_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "referral_referrer_id_user_id_fk": { + "name": "referral_referrer_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referrer_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "referral_referred_id_user_id_fk": { + "name": "referral_referred_id_user_id_fk", + "tableFrom": "referral", + "tableTo": "user", + "columnsFrom": [ + "referred_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "referral_referrer_id_referred_id_pk": { + "name": "referral_referrer_id_referred_id_pk", + "columns": [ + "referrer_id", + "referred_id" + ] + } + }, + "uniqueConstraints": {} + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "sessionToken": { + "name": "sessionToken", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "fingerprint_id": { + "name": "fingerprint_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "session_userId_user_id_fk": { + "name": "session_userId_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_fingerprint_id_fingerprint_id_fk": { + "name": "session_fingerprint_id_fingerprint_id_fk", + "tableFrom": "session", + "tableTo": "fingerprint", + "columnsFrom": [ + "fingerprint_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "emailVerified": { + "name": "emailVerified", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "subscription_active": { + "name": "subscription_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_price_id": { + "name": "stripe_price_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quota": { + "name": "quota", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "quota_exceeded": { + "name": "quota_exceeded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "next_quota_reset": { + "name": "next_quota_reset", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now() + INTERVAL '1 month'" + }, + "referral_code": { + "name": "referral_code", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'ref-' || gen_random_uuid()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": [ + "email" + ] + }, + "user_stripe_customer_id_unique": { + "name": "user_stripe_customer_id_unique", + "nullsNotDistinct": false, + "columns": [ + "stripe_customer_id" + ] + }, + "user_referral_code_unique": { + "name": "user_referral_code_unique", + "nullsNotDistinct": false, + "columns": [ + "referral_code" + ] + } + } + }, + "public.verificationToken": { + "name": "verificationToken", + "schema": "", + "columns": { + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires": { + "name": "expires", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": { + "verificationToken_identifier_token_pk": { + "name": "verificationToken_identifier_token_pk", + "columns": [ + "identifier", + "token" + ] + } + }, + "uniqueConstraints": {} + } + }, + "enums": { + "public.referral_status": { + "name": "referral_status", + "schema": "public", + "values": [ + "pending", + "completed" + ] + } + }, + "schemas": {}, + "sequences": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/common/src/db/migrations/meta/_journal.json b/common/src/db/migrations/meta/_journal.json index cfa3ab132..2f580c730 100644 --- a/common/src/db/migrations/meta/_journal.json +++ b/common/src/db/migrations/meta/_journal.json @@ -29,6 +29,13 @@ "when": 1727986902407, "tag": "0003_military_owl", "breakpoints": true + }, + { + "idx": 4, + "version": "7", + "when": 1728448952748, + "tag": "0004_neat_pet_avengers", + "breakpoints": true } ] } \ No newline at end of file diff --git a/common/src/db/schema.ts b/common/src/db/schema.ts index c08f71df5..29172dab4 100644 --- a/common/src/db/schema.ts +++ b/common/src/db/schema.ts @@ -9,8 +9,16 @@ import { jsonb, numeric, uuid, + pgEnum, } from 'drizzle-orm/pg-core' import type { AdapterAccount } from 'next-auth/adapters' +import { ReferralStatusValues } from '../types/referral' + +// Define the ReferralStatus enum +export const ReferralStatus = pgEnum('referral_status', [ + ReferralStatusValues[0], + ...ReferralStatusValues.slice(1), +]) export const user = pgTable('user', { id: text('id') @@ -67,7 +75,7 @@ export const referral = pgTable( referred_id: text('referred_id') .notNull() .references(() => user.id), - status: text('status').notNull().default('pending'), + status: ReferralStatus('status').notNull().default('pending'), created_at: timestamp('created_at', { mode: 'date' }) .notNull() .defaultNow(), diff --git a/common/src/types/referral.ts b/common/src/types/referral.ts new file mode 100644 index 000000000..27a51edf8 --- /dev/null +++ b/common/src/types/referral.ts @@ -0,0 +1,3 @@ +export type ReferralStatus = 'pending' | 'completed' + +export const ReferralStatusValues: ReferralStatus[] = ['pending', 'completed'] From 5c05b3c555a7378f038c8ac7c3f397b824ce153b Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 22:17:57 -0700 Subject: [PATCH 05/59] Add referrals page and navbar link This commit introduces a new referrals page where users can view and manage their referral code and referrals. It also adds a link to the referrals page in the navbar for authenticated users. Key changes: - Create new ReferralsPage component with referral code and list - Add referrals link to navbar for logged-in users - Implement basic UI for displaying referral information --- web/src/app/referrals/page.tsx | 90 ++++++++++++++++++++++++++++ web/src/components/navbar/navbar.tsx | 12 ++-- 2 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 web/src/app/referrals/page.tsx diff --git a/web/src/app/referrals/page.tsx b/web/src/app/referrals/page.tsx new file mode 100644 index 000000000..5b8df6af2 --- /dev/null +++ b/web/src/app/referrals/page.tsx @@ -0,0 +1,90 @@ +'use client' + +import { useState, useEffect } from 'react' +import { useSession } from 'next-auth/react' +import { Button } from '@/components/ui/button' +import { Input } from '@/components/ui/input' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { toast } from '@/components/ui/use-toast' +import * as schema from 'common/db/schema' + +const ReferralsPage = () => { + const { data: session } = useSession() + const [referralCode, setReferralCode] = useState('') + const [referrals, setReferrals] = useState< + Array + >([]) + + useEffect(() => { + if (session?.user) { + // Fetch user's referral code and referrals + // This is a placeholder and should be replaced with actual API call + setReferralCode('EXAMPLE123') + setReferrals([ + { id: 1, email: 'friend@example.com', status: 'pending' }, + { id: 2, email: 'colleague@example.com', status: 'completed' }, + ]) + } + }, [session]) + + const copyReferralCode = () => { + navigator.clipboard.writeText(referralCode) + toast({ + title: 'Copied!', + description: 'Referral code copied to clipboard', + }) + } + + return ( +
+

Your Referrals

+ + + Your Referral Code + + +
+ + +
+
+
+ + + Your Referrals + + + {referrals.length > 0 ? ( +
    + {referrals.map((referral) => ( +
  • + {referral.email} + + {referral.status} + +
  • + ))} +
+ ) : ( +

You haven't referred anyone yet.

+ )} +
+
+
+ ) +} + +export default ReferralsPage diff --git a/web/src/components/navbar/navbar.tsx b/web/src/components/navbar/navbar.tsx index b197554c1..5c8fb7f10 100644 --- a/web/src/components/navbar/navbar.tsx +++ b/web/src/components/navbar/navbar.tsx @@ -17,16 +17,14 @@ export const Navbar = async () => { Manicode
{session ? ( From 32aba9bafd87cf861b7d20bb9b291c9feede0e49 Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 22:30:00 -0700 Subject: [PATCH 06/59] Add API route for fetching referral data This commit introduces a new API route (/api/referrals) to retrieve referral information for authenticated users. The route fetches the user's referral code and a list of their referrals from the database. The implementation includes: - Authentication check using NextAuth - Database queries to fetch referral code and referrals - Error handling for unauthorized access and server errors This feature supports the referral system functionality in the web app. --- web/src/app/api/referrals/route.ts | 38 ++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 web/src/app/api/referrals/route.ts diff --git a/web/src/app/api/referrals/route.ts b/web/src/app/api/referrals/route.ts new file mode 100644 index 000000000..aaab21fe9 --- /dev/null +++ b/web/src/app/api/referrals/route.ts @@ -0,0 +1,38 @@ +import { NextResponse } from 'next/server' +import { getServerSession } from 'next-auth' +import { authOptions } from '../auth/[...nextauth]/auth-options' +import db from 'common/db' +import * as schema from 'common/db/schema' +import { eq } from 'drizzle-orm' + +export async function GET() { + const session = await getServerSession(authOptions) + + if (!session || !session.user) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const user = await db.query.user.findFirst({ + where: eq(schema.user.id, session.user.id), + columns: { + referral_code: true, + }, + }) + + const referrals = await db.query.referral.findMany({ + where: eq(schema.referral.referrer_id, session.user.id), + }) + + return NextResponse.json({ + referralCode: user?.referral_code, + referrals: referrals, + }) + } catch (error) { + console.error('Error fetching referral data:', error) + return NextResponse.json( + { error: 'Internal Server Error' }, + { status: 500 } + ) + } +} From bcbb4c0d5a3bf568610576adf353f27928273bfe Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Tue, 8 Oct 2024 22:44:58 -0700 Subject: [PATCH 07/59] Implement TanStack Query for referral data fetching This commit introduces TanStack Query for efficient data fetching and state management in the referrals feature. It includes: - Adding @tanstack/react-query as a dependency - Updating the knowledge.md file with TanStack Query usage guidelines - Refactoring the referrals API route for optimized data retrieval - Implementing TanStack Query in the ReferralsPage component - Updating the referrals UI to display fetched data correctly These changes improve performance, error handling, and code maintainability for the referrals feature. --- package.json | 1 + web/knowledge.md | 46 ++++++++++++++++++++++++++++++ web/src/app/api/referrals/route.ts | 32 ++++++++++++++------- web/src/app/referrals/page.tsx | 31 ++++++++++++++------ 4 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 web/knowledge.md diff --git a/package.json b/package.json index 9807ceee7..8de20f6d1 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "test:patch": "bun test test/__src__/patch.test.ts" }, "dependencies": { + "@tanstack/react-query": "^5.59.0", "lodash": "4.17.21", "ts-node": "^10.9.2" }, diff --git a/web/knowledge.md b/web/knowledge.md new file mode 100644 index 000000000..05586a95f --- /dev/null +++ b/web/knowledge.md @@ -0,0 +1,46 @@ + + +## Data Fetching and State Management + +### Using TanStack Query (React Query) + +For efficient data fetching and state management in React components, we use TanStack Query (formerly known as React Query). This library provides a powerful and flexible way to fetch, cache, and update data in React applications. + +Key points: +- Install the library: `npm install @tanstack/react-query` or `yarn add @tanstack/react-query` +- Use the `useQuery` hook for data fetching in components +- Implement query invalidation and refetching strategies for real-time updates +- Utilize the built-in caching mechanism to improve performance + +Example usage: + +```typescript +import { useQuery } from '@tanstack/react-query' + +const fetchReferrals = async () => { + const response = await fetch('/api/referrals') + if (!response.ok) { + throw new Error('Failed to fetch referral data') + } + return response.json() +} + +const ReferralsPage = () => { + const { data, isLoading, error } = useQuery(['referrals'], fetchReferrals) + + if (isLoading) return
Loading...
+ if (error) return
Error: {error.message}
+ + // Render referrals data +} +``` + +Benefits: +1. Automatic caching and background refetching +2. Loading and error states management +3. Simplified data synchronization across components +4. Improved performance and user experience + +When implementing new features or refactoring existing ones, prefer using TanStack Query for data fetching and state management to maintain consistency across the application. + + diff --git a/web/src/app/api/referrals/route.ts b/web/src/app/api/referrals/route.ts index aaab21fe9..a45698c43 100644 --- a/web/src/app/api/referrals/route.ts +++ b/web/src/app/api/referrals/route.ts @@ -13,20 +13,30 @@ export async function GET() { } try { - const user = await db.query.user.findFirst({ - where: eq(schema.user.id, session.user.id), - columns: { - referral_code: true, - }, - }) + const referralData = await db + .select({ + referral_code: schema.user.referral_code, + referrals: schema.referral, + }) + .from(schema.user) + .leftJoin( + schema.referral, + eq(schema.user.id, schema.referral.referrer_id) + ) + .where(eq(schema.user.id, session.user.id)) + .then((result) => { + if (result.length === 0) { + throw new Error(`No referral code found for user ${session.user?.id}`) + } - const referrals = await db.query.referral.findMany({ - where: eq(schema.referral.referrer_id, session.user.id), - }) + return result + }) + + const referrals = referralData.map((data) => data.referrals) return NextResponse.json({ - referralCode: user?.referral_code, - referrals: referrals, + referralCode: referralData[0].referral_code, + referrals, }) } catch (error) { console.error('Error fetching referral data:', error) diff --git a/web/src/app/referrals/page.tsx b/web/src/app/referrals/page.tsx index 5b8df6af2..556732872 100644 --- a/web/src/app/referrals/page.tsx +++ b/web/src/app/referrals/page.tsx @@ -17,16 +17,29 @@ const ReferralsPage = () => { useEffect(() => { if (session?.user) { - // Fetch user's referral code and referrals - // This is a placeholder and should be replaced with actual API call - setReferralCode('EXAMPLE123') - setReferrals([ - { id: 1, email: 'friend@example.com', status: 'pending' }, - { id: 2, email: 'colleague@example.com', status: 'completed' }, - ]) + fetchReferralData() } }, [session]) + const fetchReferralData = async () => { + try { + const response = await fetch('/api/referrals') + if (!response.ok) { + throw new Error('Failed to fetch referral data') + } + const data = await response.json() + setReferralCode(data.referralCode) + setReferrals(data.referrals) + } catch (error) { + console.error('Error fetching referral data:', error) + toast({ + title: 'Error', + description: 'Failed to load referral data. Please try again later.', + variant: 'destructive', + }) + } + } + const copyReferralCode = () => { navigator.clipboard.writeText(referralCode) toast({ @@ -62,10 +75,10 @@ const ReferralsPage = () => {
+ + +
+ +
+ Your Referral Link +
+
+ {loading ? ( + + ) : ( + + )} + -

referred you.

-
- {CreditsBadge(CREDITS_REFERRAL_BONUS)} + {data?.referredBy && ( + + + + You claimed a referral bonus. You + both rock! 🤘 + + + +
+
+ +

referred you.

- - - ) : ( - <> - - - Enter A Referral Code - - - -
- {loading ? ( - - ) : ( - <> - setInputCode(e.target.value)} - placeholder="Enter referral code" - /> - - - )} -
-
- - )} - + {CreditsBadge(CREDITS_REFERRAL_BONUS)} +
+
+
+ )} Your Referrals @@ -232,9 +167,30 @@ const ReferralsPage = () => {
+ + +

+ To refer, ask your friend to follow these steps: +

+
    +
  1. + Install Manicode globally: +
    +                            npm i -g manicode
    +                          
    +
  2. +
  3. + Run Manicode +
    +                            manicode
    +                          
    +
  4. +
  5. Paste your referral code in the CLI and log in.
  6. +
+
-
+ {/*
Your Referral Link @@ -261,7 +217,7 @@ const ReferralsPage = () => {
-
+
*/} ) ) From dd0092dace4c3e593b2d9cf6189ff078c1cf1051 Mon Sep 17 00:00:00 2001 From: Brandon Chen Date: Thu, 10 Oct 2024 16:23:17 -0700 Subject: [PATCH 33/59] Implement referral code handling and logout functionality - Add logout command to CLI - Implement referral code redemption for logged-in users - Refactor login process and referral code handling - Update onboarding page to use new referral code redemption - Simplify referrals page and footer component --- npm-app/src/cli.ts | 6 +- npm-app/src/client.ts | 53 +++++++++- web/src/app/api/referrals/route.ts | 33 ++++-- web/src/app/onboard/page.tsx | 158 +++++++++++------------------ web/src/app/referrals/page.tsx | 2 +- web/src/components/footer.tsx | 8 +- 6 files changed, 138 insertions(+), 122 deletions(-) diff --git a/npm-app/src/cli.ts b/npm-app/src/cli.ts index bf0627a07..354c31e50 100644 --- a/npm-app/src/cli.ts +++ b/npm-app/src/cli.ts @@ -277,8 +277,12 @@ export class CLI { if (userInput === 'login' || userInput === 'signin') { await this.client.login() return + } else if (userInput === 'logout' || userInput === 'signout') { + await this.client.logout() + this.rl.prompt() + return } else if (userInput.startsWith('ref-')) { - await this.client.login(userInput) + await this.client.handleReferralCode(userInput.trim()) return } else if (userInput === 'usage' || userInput === 'credits') { this.client.getUsage() diff --git a/npm-app/src/client.ts b/npm-app/src/client.ts index 24a3d21ff..0a0170e58 100644 --- a/npm-app/src/client.ts +++ b/npm-app/src/client.ts @@ -1,4 +1,4 @@ -import { yellow, red } from 'picocolors' +import { yellow, red, green } from 'picocolors' import { APIRealtimeClient } from 'common/websockets/websocket-client' import { getFiles, @@ -10,13 +10,15 @@ import { CREDENTIALS_PATH, User, userFromJson } from 'common/util/credentials' import { ChatStorage } from './chat-storage' import { FileChanges, Message } from 'common/actions' import { toolHandlers } from './tool-handlers' -import { CREDITS_USAGE_LIMITS, TOOL_RESULT_MARKER } from 'common/constants' +import { + CREDITS_REFERRAL_BONUS, + CREDITS_USAGE_LIMITS, + TOOL_RESULT_MARKER, +} from 'common/constants' import { fingerprintId } from './config' import { uniq } from 'lodash' -import { spawn } from 'child_process' import path from 'path' import * as fs from 'fs' -import { sleep } from 'common/util/helpers' import { match, P } from 'ts-pattern' export class Client { @@ -55,7 +57,44 @@ export class Client { this.setupSubscriptions() } - async login(referralCode?: string) { + async handleReferralCode(referralCode: string) { + if (this.user) { + // User is logged in, so attempt to redeem referral code directly + try { + const redeemReferralResp = await fetch( + `${process.env.NEXT_PUBLIC_APP_URL}/api/referrals`, + { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${this.user.authToken}`, + }, + body: JSON.stringify({ referralCode }), + } + ) + const respJson = await redeemReferralResp.json() + if (redeemReferralResp.ok) { + green( + `Easy peasy, you've earned an extra ${respJson.creditsRedeemed} credits!` + ) + console.log( + `pssst: you can also refer new users and earn ${CREDITS_REFERRAL_BONUS} for each referral! Go to ${process.env.NEXT_PUBLIC_APP_URL}/referrals to see your referral history and referral code!` + ) + this.getUsage() + } else { + throw new Error(respJson.error) + } + } catch (e) { + const error = e as Error + console.error(red('Error: ' + error.message)) + this.returnControlToUser() + } + } else { + await this.login(referralCode) + } + } + + async logout() { if (this.user) { // If there was an existing user, clear their existing state this.webSocket.sendAction({ @@ -68,9 +107,13 @@ export class Client { // delete credentials file fs.unlinkSync(CREDENTIALS_PATH) + console.log(`Logged you out of your account (${this.user.name})`) this.user = undefined } + } + async login(referralCode?: string) { + this.logout() this.webSocket.sendAction({ type: 'login-code-request', fingerprintId, diff --git a/web/src/app/api/referrals/route.ts b/web/src/app/api/referrals/route.ts index 7cdbc981d..3c455afc4 100644 --- a/web/src/app/api/referrals/route.ts +++ b/web/src/app/api/referrals/route.ts @@ -113,16 +113,11 @@ export async function GET() { } } -export async function POST(request: Request) { - const session = await getServerSession(authOptions) - const userId = session?.user?.id - if (!session || !userId) { - return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) - } - +export async function redeemReferralCode( + referralCode: string, + userId: string +): Promise { try { - const { referralCode } = await request.json() - // Check if the user has already used a referral code const existingReferral = await db .select() @@ -203,7 +198,14 @@ export async function POST(request: Request) { .where(or(eq(schema.user.id, referrer.id), eq(schema.user.id, userId))) }) - return NextResponse.json({ success: true }) + return NextResponse.json( + { + credits_redeemed: CREDITS_REFERRAL_BONUS, + }, + { + status: 200, + } + ) } catch (error) { console.error('Error applying referral code:', error) return NextResponse.json( @@ -212,3 +214,14 @@ export async function POST(request: Request) { ) } } + +export async function POST(request: Request) { + const session = await getServerSession(authOptions) + const userId = session?.user?.id + if (!session || !userId) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { referralCode } = await request.json() + return redeemReferralCode(referralCode, userId) +} diff --git a/web/src/app/onboard/page.tsx b/web/src/app/onboard/page.tsx index 24061e301..bcd67ae5b 100644 --- a/web/src/app/onboard/page.tsx +++ b/web/src/app/onboard/page.tsx @@ -3,15 +3,16 @@ import { toast } from '@/components/ui/use-toast' import { getServerSession } from 'next-auth' import Image from 'next/image' -import { notFound, redirect } from 'next/navigation' +import { redirect } from 'next/navigation' import db from 'common/db' import * as schema from 'common/db/schema' -import { and, eq, sql } from 'drizzle-orm' -import { CREDITS_REFERRAL_BONUS, MAX_DATE } from 'common/src/constants' +import { and, eq } from 'drizzle-orm' +import { MAX_DATE } from 'common/src/constants' import { authOptions } from '../api/auth/[...nextauth]/auth-options' import { genAuthCode } from 'common/util/credentials' import { env } from '@/env.mjs' import CardWithBeams from '@/components/card-with-beams' +import { redeemReferralCode } from '../api/referrals/route' interface PageProps { searchParams: { @@ -131,106 +132,67 @@ const Onboard = async ({ searchParams }: PageProps) => { }) .returning({ userId: schema.session.userId }) - // If referral code is present, attempt to create a referral - let didInsertReferralCode: boolean | undefined = undefined - if (referralCode) { - const referrer = await tx - .select() - .from(schema.user) - .where(eq(schema.user.referral_code, referralCode)) - .limit(1) - .then((referrers) => { - if (referrers.length !== 1) { - return - } - return referrers[0] - }) - - if (!referrer) { - didInsertReferralCode = false - } else { - await tx.insert(schema.referral).values({ - referrer_id: referrer.id, - referred_id: user.id, - status: 'completed', - credits: CREDITS_REFERRAL_BONUS, - created_at: new Date(), - completed_at: new Date(), - }) - - await tx - .update(schema.user) - .set({ - quota: sql`${schema.user.quota} + ${CREDITS_REFERRAL_BONUS}`, - }) - .where(eq(schema.user.id, referrer.id)) - - await tx - .update(schema.user) - .set({ - quota: sql`${schema.user.quota} + ${CREDITS_REFERRAL_BONUS}`, - }) - .where(eq(schema.user.id, user.id)) + return !!session.length + }) - didInsertReferralCode = true + let redeemReferralMessage = <> + if (referralCode) { + try { + const redeemReferralResp = await redeemReferralCode(referralCode, user.id) + const respJson = await redeemReferralResp.json() + if (!redeemReferralResp.ok) { + throw new Error(respJson.error) } + redeemReferralMessage = ( +

+ `You've earned an extra ${respJson.credits_redeemed} credits from your + referral code!` +

+ ) + } catch (e) { + console.error(e) + const error = e as Error + redeemReferralMessage = ( +
+

Uh-oh, we couldn't apply your referral code. {error.message}.

+

+ Please try again and reach out to {env.NEXT_PUBLIC_SUPPORT_EMAIL} if + the problem persists. +

+
+ ) } - - return { - didInsertFingerprint: !!session.length, - didInsertReferralCode, - } - }) + } // Render the result - return didInsert.didInsertFingerprint - ? CardWithBeams({ - title: 'Nicely done!', - description: - 'Feel free to close this window and head back to your terminal. Enjoy the extra api credits!', - content: ( - <> - Successful authentication - {didInsert.didInsertReferralCode ? ( -

- You've earned an extra {CREDITS_REFERRAL_BONUS} credits from - your referral code! -

- ) : ( -

- Slight hiccup: we couldn\'t automatically apply your referral - code. Can you go to ${env.NEXT_PUBLIC_APP_URL}/referrals and - manually apply it? -

- )} - - ), - }) - : CardWithBeams({ - title: 'Uh-oh, spaghettio!', - description: 'Something went wrong.', - content: ( - <> - {didInsert.didInsertReferralCode ? ( -

- Please try again and reach out to{' '} - {env.NEXT_PUBLIC_SUPPORT_EMAIL} if the problem persists. -

- ) : ( -

- Slight hiccup: we couldn\'t automatically apply your referral - code. Can you go to ${env.NEXT_PUBLIC_APP_URL}/referrals and - manually apply it? -

- )} - - ), - }) + if (didInsert) { + return CardWithBeams({ + title: 'Nicely done!', + description: + 'Feel free to close this window and head back to your terminal.', + content: ( +
+ Successful authentication + {redeemReferralMessage} +
+ ), + }) + } + return CardWithBeams({ + title: 'Uh-oh, spaghettio!', + description: 'Something went wrong.', + content: ( +

+ Not sure what happened with creating your user. Please try again and + reach out to {env.NEXT_PUBLIC_SUPPORT_EMAIL} if the problem persists. +

+ ), + }) } export default Onboard diff --git a/web/src/app/referrals/page.tsx b/web/src/app/referrals/page.tsx index ff1b28fad..925968839 100644 --- a/web/src/app/referrals/page.tsx +++ b/web/src/app/referrals/page.tsx @@ -6,7 +6,7 @@ import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { toast } from '@/components/ui/use-toast' -import { ReferralData } from '../api/referrals/route' +import { ReferralData } from '@/app/api/referrals/route' import { Skeleton } from '@/components/ui/skeleton' import { match, P } from 'ts-pattern' import { env } from '@/env.mjs' diff --git a/web/src/components/footer.tsx b/web/src/components/footer.tsx index ac776f2b8..afc13e471 100644 --- a/web/src/components/footer.tsx +++ b/web/src/components/footer.tsx @@ -4,13 +4,7 @@ import Link from 'next/link' export const Footer = () => { return (