├── .eslintrc.json ├── .gitignore ├── README.md ├── components.json ├── drizzle.config.ts ├── drizzle ├── 0000_past_madripoor.sql ├── 0001_round_hammerhead.sql ├── 0002_kind_ultragirl.sql └── meta │ ├── 0000_snapshot.json │ ├── 0001_snapshot.json │ ├── 0002_snapshot.json │ └── _journal.json ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── _actions │ │ └── thread.ts │ ├── api │ │ └── auth │ │ │ └── [...nextauth] │ │ │ └── route.ts │ ├── dialogue │ │ └── [id] │ │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ ├── Sidebar.tsx │ ├── client │ │ ├── AddThreadForm.tsx │ │ ├── AuthAlert.tsx │ │ ├── ClientToast.tsx │ │ ├── RepliesComponent.tsx │ │ ├── SignIn.tsx │ │ ├── SignOut.tsx │ │ ├── TextArea.tsx │ │ └── ThreadList.tsx │ ├── theme-provider.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── toast.tsx │ │ ├── toaster.tsx │ │ └── use-toast.ts ├── db │ ├── index.ts │ └── schema.ts ├── lib │ ├── auth.ts │ └── utils.ts ├── store │ └── useLoginDialogStore.ts ├── types │ └── next-auth.d.ts └── validations │ └── thread.ts ├── tailwind.config.js ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | # local env files 27 | .env 28 | .env*.local 29 | # vercel 30 | .vercel 31 | 32 | # typescript 33 | *.tsbuildinfo 34 | next-env.d.ts 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | # threads-demon 36 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/app/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true 11 | }, 12 | "aliases": { 13 | "components": "@/components", 14 | "utils": "@/lib/utils" 15 | } 16 | } -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "drizzle-kit"; 2 | import "dotenv/config"; 3 | 4 | export default { 5 | schema: "./src/db/schema.ts", 6 | driver: "pg", 7 | out: "./drizzle", 8 | dbCredentials: { 9 | connectionString: process.env.DRIZZLE_DATABASE_URL!, 10 | }, 11 | } satisfies Config; 12 | -------------------------------------------------------------------------------- /drizzle/0000_past_madripoor.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "account" ( 2 | "userId" text NOT NULL, 3 | "type" text NOT NULL, 4 | "provider" text NOT NULL, 5 | "providerAccountId" text NOT NULL, 6 | "refresh_token" text, 7 | "access_token" text, 8 | "expires_at" integer, 9 | "token_type" text, 10 | "scope" text, 11 | "id_token" text, 12 | "session_state" text, 13 | CONSTRAINT account_provider_providerAccountId PRIMARY KEY("provider","providerAccountId") 14 | ); 15 | --> statement-breakpoint 16 | CREATE TABLE IF NOT EXISTS "session" ( 17 | "sessionToken" text PRIMARY KEY NOT NULL, 18 | "userId" text NOT NULL, 19 | "expires" timestamp NOT NULL 20 | ); 21 | --> statement-breakpoint 22 | CREATE TABLE IF NOT EXISTS "user" ( 23 | "id" text PRIMARY KEY NOT NULL, 24 | "name" text, 25 | "email" text NOT NULL, 26 | "emailVerified" timestamp, 27 | "image" text 28 | ); 29 | --> statement-breakpoint 30 | CREATE TABLE IF NOT EXISTS "verificationToken" ( 31 | "identifier" text NOT NULL, 32 | "token" text NOT NULL, 33 | "expires" timestamp NOT NULL, 34 | CONSTRAINT verificationToken_identifier_token PRIMARY KEY("identifier","token") 35 | ); 36 | --> statement-breakpoint 37 | DO $$ BEGIN 38 | ALTER TABLE "account" ADD CONSTRAINT "account_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; 39 | EXCEPTION 40 | WHEN duplicate_object THEN null; 41 | END $$; 42 | --> statement-breakpoint 43 | DO $$ BEGIN 44 | ALTER TABLE "session" ADD CONSTRAINT "session_userId_user_id_fk" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE cascade ON UPDATE no action; 45 | EXCEPTION 46 | WHEN duplicate_object THEN null; 47 | END $$; 48 | -------------------------------------------------------------------------------- /drizzle/0001_round_hammerhead.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "user" ADD COLUMN "username" text;--> statement-breakpoint 2 | ALTER TABLE "user" ADD CONSTRAINT "user_username_unique" UNIQUE("username"); -------------------------------------------------------------------------------- /drizzle/0002_kind_ultragirl.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS "thread" ( 2 | "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, 3 | "text" text NOT NULL, 4 | "user_id" text NOT NULL, 5 | "created_at" timestamp DEFAULT now(), 6 | "parent_id" text, 7 | "dialogue_id" uuid 8 | ); 9 | -------------------------------------------------------------------------------- /drizzle/meta/0000_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "id": "7288581e-7590-4449-aef0-ad7b7849a93e", 5 | "prevId": "00000000-0000-0000-0000-000000000000", 6 | "tables": { 7 | "account": { 8 | "name": "account", 9 | "schema": "", 10 | "columns": { 11 | "userId": { 12 | "name": "userId", 13 | "type": "text", 14 | "primaryKey": false, 15 | "notNull": true 16 | }, 17 | "type": { 18 | "name": "type", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "provider": { 24 | "name": "provider", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "providerAccountId": { 30 | "name": "providerAccountId", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": true 34 | }, 35 | "refresh_token": { 36 | "name": "refresh_token", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": false 40 | }, 41 | "access_token": { 42 | "name": "access_token", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": false 46 | }, 47 | "expires_at": { 48 | "name": "expires_at", 49 | "type": "integer", 50 | "primaryKey": false, 51 | "notNull": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "account_userId_user_id_fk": { 81 | "name": "account_userId_user_id_fk", 82 | "tableFrom": "account", 83 | "tableTo": "user", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade", 91 | "onUpdate": "no action" 92 | } 93 | }, 94 | "compositePrimaryKeys": { 95 | "account_provider_providerAccountId": { 96 | "name": "account_provider_providerAccountId", 97 | "columns": [ 98 | "provider", 99 | "providerAccountId" 100 | ] 101 | } 102 | }, 103 | "uniqueConstraints": {} 104 | }, 105 | "session": { 106 | "name": "session", 107 | "schema": "", 108 | "columns": { 109 | "sessionToken": { 110 | "name": "sessionToken", 111 | "type": "text", 112 | "primaryKey": true, 113 | "notNull": true 114 | }, 115 | "userId": { 116 | "name": "userId", 117 | "type": "text", 118 | "primaryKey": false, 119 | "notNull": true 120 | }, 121 | "expires": { 122 | "name": "expires", 123 | "type": "timestamp", 124 | "primaryKey": false, 125 | "notNull": true 126 | } 127 | }, 128 | "indexes": {}, 129 | "foreignKeys": { 130 | "session_userId_user_id_fk": { 131 | "name": "session_userId_user_id_fk", 132 | "tableFrom": "session", 133 | "tableTo": "user", 134 | "columnsFrom": [ 135 | "userId" 136 | ], 137 | "columnsTo": [ 138 | "id" 139 | ], 140 | "onDelete": "cascade", 141 | "onUpdate": "no action" 142 | } 143 | }, 144 | "compositePrimaryKeys": {}, 145 | "uniqueConstraints": {} 146 | }, 147 | "user": { 148 | "name": "user", 149 | "schema": "", 150 | "columns": { 151 | "id": { 152 | "name": "id", 153 | "type": "text", 154 | "primaryKey": true, 155 | "notNull": true 156 | }, 157 | "name": { 158 | "name": "name", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": false 162 | }, 163 | "email": { 164 | "name": "email", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "emailVerified": { 170 | "name": "emailVerified", 171 | "type": "timestamp", 172 | "primaryKey": false, 173 | "notNull": false 174 | }, 175 | "image": { 176 | "name": "image", 177 | "type": "text", 178 | "primaryKey": false, 179 | "notNull": false 180 | } 181 | }, 182 | "indexes": {}, 183 | "foreignKeys": {}, 184 | "compositePrimaryKeys": {}, 185 | "uniqueConstraints": {} 186 | }, 187 | "verificationToken": { 188 | "name": "verificationToken", 189 | "schema": "", 190 | "columns": { 191 | "identifier": { 192 | "name": "identifier", 193 | "type": "text", 194 | "primaryKey": false, 195 | "notNull": true 196 | }, 197 | "token": { 198 | "name": "token", 199 | "type": "text", 200 | "primaryKey": false, 201 | "notNull": true 202 | }, 203 | "expires": { 204 | "name": "expires", 205 | "type": "timestamp", 206 | "primaryKey": false, 207 | "notNull": true 208 | } 209 | }, 210 | "indexes": {}, 211 | "foreignKeys": {}, 212 | "compositePrimaryKeys": { 213 | "verificationToken_identifier_token": { 214 | "name": "verificationToken_identifier_token", 215 | "columns": [ 216 | "identifier", 217 | "token" 218 | ] 219 | } 220 | }, 221 | "uniqueConstraints": {} 222 | } 223 | }, 224 | "enums": {}, 225 | "schemas": {}, 226 | "_meta": { 227 | "schemas": {}, 228 | "tables": {}, 229 | "columns": {} 230 | } 231 | } -------------------------------------------------------------------------------- /drizzle/meta/0001_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "id": "8d6baaaf-a390-43c2-997c-315a78a9f107", 5 | "prevId": "7288581e-7590-4449-aef0-ad7b7849a93e", 6 | "tables": { 7 | "account": { 8 | "name": "account", 9 | "schema": "", 10 | "columns": { 11 | "userId": { 12 | "name": "userId", 13 | "type": "text", 14 | "primaryKey": false, 15 | "notNull": true 16 | }, 17 | "type": { 18 | "name": "type", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "provider": { 24 | "name": "provider", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "providerAccountId": { 30 | "name": "providerAccountId", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": true 34 | }, 35 | "refresh_token": { 36 | "name": "refresh_token", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": false 40 | }, 41 | "access_token": { 42 | "name": "access_token", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": false 46 | }, 47 | "expires_at": { 48 | "name": "expires_at", 49 | "type": "integer", 50 | "primaryKey": false, 51 | "notNull": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "account_userId_user_id_fk": { 81 | "name": "account_userId_user_id_fk", 82 | "tableFrom": "account", 83 | "tableTo": "user", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade", 91 | "onUpdate": "no action" 92 | } 93 | }, 94 | "compositePrimaryKeys": { 95 | "account_provider_providerAccountId": { 96 | "name": "account_provider_providerAccountId", 97 | "columns": [ 98 | "provider", 99 | "providerAccountId" 100 | ] 101 | } 102 | }, 103 | "uniqueConstraints": {} 104 | }, 105 | "session": { 106 | "name": "session", 107 | "schema": "", 108 | "columns": { 109 | "sessionToken": { 110 | "name": "sessionToken", 111 | "type": "text", 112 | "primaryKey": true, 113 | "notNull": true 114 | }, 115 | "userId": { 116 | "name": "userId", 117 | "type": "text", 118 | "primaryKey": false, 119 | "notNull": true 120 | }, 121 | "expires": { 122 | "name": "expires", 123 | "type": "timestamp", 124 | "primaryKey": false, 125 | "notNull": true 126 | } 127 | }, 128 | "indexes": {}, 129 | "foreignKeys": { 130 | "session_userId_user_id_fk": { 131 | "name": "session_userId_user_id_fk", 132 | "tableFrom": "session", 133 | "tableTo": "user", 134 | "columnsFrom": [ 135 | "userId" 136 | ], 137 | "columnsTo": [ 138 | "id" 139 | ], 140 | "onDelete": "cascade", 141 | "onUpdate": "no action" 142 | } 143 | }, 144 | "compositePrimaryKeys": {}, 145 | "uniqueConstraints": {} 146 | }, 147 | "user": { 148 | "name": "user", 149 | "schema": "", 150 | "columns": { 151 | "id": { 152 | "name": "id", 153 | "type": "text", 154 | "primaryKey": true, 155 | "notNull": true 156 | }, 157 | "name": { 158 | "name": "name", 159 | "type": "text", 160 | "primaryKey": false, 161 | "notNull": false 162 | }, 163 | "email": { 164 | "name": "email", 165 | "type": "text", 166 | "primaryKey": false, 167 | "notNull": true 168 | }, 169 | "emailVerified": { 170 | "name": "emailVerified", 171 | "type": "timestamp", 172 | "primaryKey": false, 173 | "notNull": false 174 | }, 175 | "image": { 176 | "name": "image", 177 | "type": "text", 178 | "primaryKey": false, 179 | "notNull": false 180 | }, 181 | "username": { 182 | "name": "username", 183 | "type": "text", 184 | "primaryKey": false, 185 | "notNull": false 186 | } 187 | }, 188 | "indexes": {}, 189 | "foreignKeys": {}, 190 | "compositePrimaryKeys": {}, 191 | "uniqueConstraints": { 192 | "user_username_unique": { 193 | "name": "user_username_unique", 194 | "nullsNotDistinct": false, 195 | "columns": [ 196 | "username" 197 | ] 198 | } 199 | } 200 | }, 201 | "verificationToken": { 202 | "name": "verificationToken", 203 | "schema": "", 204 | "columns": { 205 | "identifier": { 206 | "name": "identifier", 207 | "type": "text", 208 | "primaryKey": false, 209 | "notNull": true 210 | }, 211 | "token": { 212 | "name": "token", 213 | "type": "text", 214 | "primaryKey": false, 215 | "notNull": true 216 | }, 217 | "expires": { 218 | "name": "expires", 219 | "type": "timestamp", 220 | "primaryKey": false, 221 | "notNull": true 222 | } 223 | }, 224 | "indexes": {}, 225 | "foreignKeys": {}, 226 | "compositePrimaryKeys": { 227 | "verificationToken_identifier_token": { 228 | "name": "verificationToken_identifier_token", 229 | "columns": [ 230 | "identifier", 231 | "token" 232 | ] 233 | } 234 | }, 235 | "uniqueConstraints": {} 236 | } 237 | }, 238 | "enums": {}, 239 | "schemas": {}, 240 | "_meta": { 241 | "schemas": {}, 242 | "tables": {}, 243 | "columns": {} 244 | } 245 | } -------------------------------------------------------------------------------- /drizzle/meta/0002_snapshot.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "id": "e24cce00-d88b-4aa5-85ad-f64935418b31", 5 | "prevId": "8d6baaaf-a390-43c2-997c-315a78a9f107", 6 | "tables": { 7 | "account": { 8 | "name": "account", 9 | "schema": "", 10 | "columns": { 11 | "userId": { 12 | "name": "userId", 13 | "type": "text", 14 | "primaryKey": false, 15 | "notNull": true 16 | }, 17 | "type": { 18 | "name": "type", 19 | "type": "text", 20 | "primaryKey": false, 21 | "notNull": true 22 | }, 23 | "provider": { 24 | "name": "provider", 25 | "type": "text", 26 | "primaryKey": false, 27 | "notNull": true 28 | }, 29 | "providerAccountId": { 30 | "name": "providerAccountId", 31 | "type": "text", 32 | "primaryKey": false, 33 | "notNull": true 34 | }, 35 | "refresh_token": { 36 | "name": "refresh_token", 37 | "type": "text", 38 | "primaryKey": false, 39 | "notNull": false 40 | }, 41 | "access_token": { 42 | "name": "access_token", 43 | "type": "text", 44 | "primaryKey": false, 45 | "notNull": false 46 | }, 47 | "expires_at": { 48 | "name": "expires_at", 49 | "type": "integer", 50 | "primaryKey": false, 51 | "notNull": false 52 | }, 53 | "token_type": { 54 | "name": "token_type", 55 | "type": "text", 56 | "primaryKey": false, 57 | "notNull": false 58 | }, 59 | "scope": { 60 | "name": "scope", 61 | "type": "text", 62 | "primaryKey": false, 63 | "notNull": false 64 | }, 65 | "id_token": { 66 | "name": "id_token", 67 | "type": "text", 68 | "primaryKey": false, 69 | "notNull": false 70 | }, 71 | "session_state": { 72 | "name": "session_state", 73 | "type": "text", 74 | "primaryKey": false, 75 | "notNull": false 76 | } 77 | }, 78 | "indexes": {}, 79 | "foreignKeys": { 80 | "account_userId_user_id_fk": { 81 | "name": "account_userId_user_id_fk", 82 | "tableFrom": "account", 83 | "tableTo": "user", 84 | "columnsFrom": [ 85 | "userId" 86 | ], 87 | "columnsTo": [ 88 | "id" 89 | ], 90 | "onDelete": "cascade", 91 | "onUpdate": "no action" 92 | } 93 | }, 94 | "compositePrimaryKeys": { 95 | "account_provider_providerAccountId": { 96 | "name": "account_provider_providerAccountId", 97 | "columns": [ 98 | "provider", 99 | "providerAccountId" 100 | ] 101 | } 102 | }, 103 | "uniqueConstraints": {} 104 | }, 105 | "session": { 106 | "name": "session", 107 | "schema": "", 108 | "columns": { 109 | "sessionToken": { 110 | "name": "sessionToken", 111 | "type": "text", 112 | "primaryKey": true, 113 | "notNull": true 114 | }, 115 | "userId": { 116 | "name": "userId", 117 | "type": "text", 118 | "primaryKey": false, 119 | "notNull": true 120 | }, 121 | "expires": { 122 | "name": "expires", 123 | "type": "timestamp", 124 | "primaryKey": false, 125 | "notNull": true 126 | } 127 | }, 128 | "indexes": {}, 129 | "foreignKeys": { 130 | "session_userId_user_id_fk": { 131 | "name": "session_userId_user_id_fk", 132 | "tableFrom": "session", 133 | "tableTo": "user", 134 | "columnsFrom": [ 135 | "userId" 136 | ], 137 | "columnsTo": [ 138 | "id" 139 | ], 140 | "onDelete": "cascade", 141 | "onUpdate": "no action" 142 | } 143 | }, 144 | "compositePrimaryKeys": {}, 145 | "uniqueConstraints": {} 146 | }, 147 | "thread": { 148 | "name": "thread", 149 | "schema": "", 150 | "columns": { 151 | "id": { 152 | "name": "id", 153 | "type": "uuid", 154 | "primaryKey": true, 155 | "notNull": true, 156 | "default": "gen_random_uuid()" 157 | }, 158 | "text": { 159 | "name": "text", 160 | "type": "text", 161 | "primaryKey": false, 162 | "notNull": true 163 | }, 164 | "user_id": { 165 | "name": "user_id", 166 | "type": "text", 167 | "primaryKey": false, 168 | "notNull": true 169 | }, 170 | "created_at": { 171 | "name": "created_at", 172 | "type": "timestamp", 173 | "primaryKey": false, 174 | "notNull": false, 175 | "default": "now()" 176 | }, 177 | "parent_id": { 178 | "name": "parent_id", 179 | "type": "text", 180 | "primaryKey": false, 181 | "notNull": false 182 | }, 183 | "dialogue_id": { 184 | "name": "dialogue_id", 185 | "type": "uuid", 186 | "primaryKey": false, 187 | "notNull": false 188 | } 189 | }, 190 | "indexes": {}, 191 | "foreignKeys": {}, 192 | "compositePrimaryKeys": {}, 193 | "uniqueConstraints": {} 194 | }, 195 | "user": { 196 | "name": "user", 197 | "schema": "", 198 | "columns": { 199 | "id": { 200 | "name": "id", 201 | "type": "text", 202 | "primaryKey": true, 203 | "notNull": true 204 | }, 205 | "name": { 206 | "name": "name", 207 | "type": "text", 208 | "primaryKey": false, 209 | "notNull": false 210 | }, 211 | "email": { 212 | "name": "email", 213 | "type": "text", 214 | "primaryKey": false, 215 | "notNull": true 216 | }, 217 | "emailVerified": { 218 | "name": "emailVerified", 219 | "type": "timestamp", 220 | "primaryKey": false, 221 | "notNull": false 222 | }, 223 | "image": { 224 | "name": "image", 225 | "type": "text", 226 | "primaryKey": false, 227 | "notNull": false 228 | }, 229 | "username": { 230 | "name": "username", 231 | "type": "text", 232 | "primaryKey": false, 233 | "notNull": false 234 | } 235 | }, 236 | "indexes": {}, 237 | "foreignKeys": {}, 238 | "compositePrimaryKeys": {}, 239 | "uniqueConstraints": { 240 | "user_username_unique": { 241 | "name": "user_username_unique", 242 | "nullsNotDistinct": false, 243 | "columns": [ 244 | "username" 245 | ] 246 | } 247 | } 248 | }, 249 | "verificationToken": { 250 | "name": "verificationToken", 251 | "schema": "", 252 | "columns": { 253 | "identifier": { 254 | "name": "identifier", 255 | "type": "text", 256 | "primaryKey": false, 257 | "notNull": true 258 | }, 259 | "token": { 260 | "name": "token", 261 | "type": "text", 262 | "primaryKey": false, 263 | "notNull": true 264 | }, 265 | "expires": { 266 | "name": "expires", 267 | "type": "timestamp", 268 | "primaryKey": false, 269 | "notNull": true 270 | } 271 | }, 272 | "indexes": {}, 273 | "foreignKeys": {}, 274 | "compositePrimaryKeys": { 275 | "verificationToken_identifier_token": { 276 | "name": "verificationToken_identifier_token", 277 | "columns": [ 278 | "identifier", 279 | "token" 280 | ] 281 | } 282 | }, 283 | "uniqueConstraints": {} 284 | } 285 | }, 286 | "enums": {}, 287 | "schemas": {}, 288 | "_meta": { 289 | "schemas": {}, 290 | "tables": {}, 291 | "columns": {} 292 | } 293 | } -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "5", 3 | "dialect": "pg", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "5", 8 | "when": 1692008223015, 9 | "tag": "0000_past_madripoor", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "5", 15 | "when": 1692081127736, 16 | "tag": "0001_round_hammerhead", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "5", 22 | "when": 1692348935935, 23 | "tag": "0002_kind_ultragirl", 24 | "breakpoints": true 25 | } 26 | ] 27 | } -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | images: { 4 | domains: ['lh3.googleusercontent.com', "icon-library.com"], 5 | }, 6 | experimental: { 7 | serverActions: true, 8 | }, 9 | } 10 | 11 | module.exports = nextConfig 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "threads-clone-demon", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint", 10 | "db:generate": "drizzle-kit generate:pg", 11 | "db:push": "drizzle-kit push:pg", 12 | "db:introspect": "drizzle-kit introspect:pg" 13 | }, 14 | "dependencies": { 15 | "@auth/drizzle-adapter": "^0.3.1", 16 | "@hookform/resolvers": "^3.2.0", 17 | "@neondatabase/serverless": "^0.5.7", 18 | "@radix-ui/react-dialog": "^1.0.4", 19 | "@radix-ui/react-dropdown-menu": "^2.0.5", 20 | "@radix-ui/react-icons": "^1.3.0", 21 | "@radix-ui/react-label": "^2.0.2", 22 | "@radix-ui/react-slot": "^1.0.2", 23 | "@radix-ui/react-toast": "^1.1.4", 24 | "@types/node": "20.5.0", 25 | "@types/react": "18.2.20", 26 | "@types/react-dom": "18.2.7", 27 | "@types/uuid": "^9.0.2", 28 | "autoprefixer": "10.4.15", 29 | "class-variance-authority": "^0.7.0", 30 | "clsx": "^2.0.0", 31 | "dotenv": "^16.3.1", 32 | "drizzle-orm": "^0.28.2", 33 | "eslint": "8.47.0", 34 | "eslint-config-next": "13.4.13", 35 | "framer-motion": "^10.16.0", 36 | "lucide-react": "^0.263.1", 37 | "nanoid": "^4.0.2", 38 | "next": "13.4.14-canary.0", 39 | "next-auth": "^4.23.0", 40 | "next-themes": "^0.2.1", 41 | "postcss": "8.4.27", 42 | "react": "18.2.0", 43 | "react-dom": "18.2.0", 44 | "react-hook-form": "^7.45.4", 45 | "react-icons": "^4.10.1", 46 | "react-textarea-autosize": "^8.5.2", 47 | "sonner": "^0.6.2", 48 | "tailwind-merge": "^1.14.0", 49 | "tailwindcss": "3.3.3", 50 | "tailwindcss-animate": "^1.0.6", 51 | "typescript": "5.1.6", 52 | "uuid": "^9.0.0", 53 | "zod": "^3.22.1", 54 | "zustand": "^4.4.1" 55 | }, 56 | "devDependencies": { 57 | "drizzle-kit": "^0.19.12" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/_actions/thread.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | import { db } from "@/db"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | import { getAuthSession } from "@/lib/auth"; 5 | import { addThreadsSchema } from "@/validations/thread"; 6 | import { z } from "zod"; 7 | import { Thread, threads } from "@/db/schema"; 8 | import { revalidatePath } from "next/cache"; 9 | import { and, eq, inArray, isNull, not } from "drizzle-orm"; 10 | import { get } from "http"; 11 | export async function addThreadsAction( 12 | input: z.infer 13 | ) { 14 | const session = await getAuthSession(); 15 | if (!session) return Error("Not logged in"); 16 | const uuids = getUuidsFromLength(input.threads.length); 17 | type ThreadNew = Omit; 18 | const result_threads: Omit = []; 19 | input.threads.forEach((thread, i) => { 20 | result_threads.push({ 21 | id: uuids[i], 22 | text: thread.text, 23 | userId: session.user.id, 24 | parentId: uuids[i - 1] || null, 25 | dialogue_id: thread.dialogueId, 26 | }); 27 | }); 28 | console.log(result_threads); 29 | await db.insert(threads).values(result_threads); 30 | revalidatePath("/"); 31 | } 32 | 33 | function getUuidsFromLength(length: number) { 34 | return Array.from({ length }, () => uuidv4()); 35 | } 36 | 37 | export async function getFeedAction() { 38 | const results = await db.query.threads.findMany({ 39 | where: isNull(threads.parentId), 40 | with: { 41 | user: { 42 | columns: { 43 | username: true, 44 | image: true, 45 | id: true, 46 | }, 47 | }, 48 | }, 49 | }); 50 | return results; 51 | } 52 | export async function getDialogueForThread(id: string) { 53 | const thread = await db.query.threads.findFirst({ 54 | where: eq(threads.id, id), 55 | with: { 56 | user: { 57 | columns: { 58 | id: true, 59 | username: true, 60 | image: true, 61 | }, 62 | }, 63 | }, 64 | }); 65 | if (!thread) throw Error("No thread found"); 66 | const dialogue_id = thread?.dialogue_id; 67 | if (!dialogue_id) throw new Error("No dialogue id found"); 68 | const dialogue = await db.query.threads.findMany({ 69 | where: and( 70 | eq(threads.dialogue_id, dialogue_id), 71 | not(eq(threads.id, id)) 72 | ), 73 | with: { 74 | user: { 75 | columns: { 76 | id: true, 77 | username: true, 78 | image: true, 79 | }, 80 | }, 81 | }, 82 | }); 83 | 84 | const ordered_threads = [thread]; 85 | 86 | const parentId_to_thread = new Map(); 87 | 88 | dialogue.forEach((thread) => { 89 | parentId_to_thread.set(thread.parentId!, thread); 90 | }); 91 | 92 | let current_thread_parent_id = thread.id; 93 | while (current_thread_parent_id) { 94 | const current_thread = parentId_to_thread.get(current_thread_parent_id); 95 | if (!current_thread) break; 96 | ordered_threads.push(current_thread); 97 | parentId_to_thread.delete(current_thread_parent_id); 98 | current_thread_parent_id = current_thread.id; 99 | } 100 | return ordered_threads; 101 | } 102 | export const deleteThreadAction = async (id: string) => { 103 | const session = await getAuthSession(); 104 | if (!session) return Error("Not logged in"); 105 | const dialogue = await getDialogueForThread(id); 106 | await db.delete(threads).where( 107 | inArray( 108 | threads.id, 109 | dialogue.map((t) => t.id) 110 | ) 111 | ); 112 | revalidatePath("/"); 113 | revalidatePath(`/dialogue/${id}`); 114 | }; 115 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/lib/auth" 2 | import NextAuth from "next-auth" 3 | 4 | const handler = NextAuth(authOptions) 5 | 6 | export { handler as GET, handler as POST } -------------------------------------------------------------------------------- /src/app/dialogue/[id]/page.tsx: -------------------------------------------------------------------------------- 1 | import { getDialogueForThread } from "@/app/_actions/thread"; 2 | import { RepliesComponent } from "@/components/client/RepliesComponent"; 3 | import Image from "next/image"; 4 | 5 | const page = async ({ 6 | params, 7 | }: { 8 | params: { 9 | id: string; 10 | }; 11 | }) => { 12 | const threads_data = await getDialogueForThread(params.id); 13 | const main_thread = threads_data[0]; 14 | const replies = threads_data.slice(1); 15 | return ( 16 |
17 |
18 |
19 |
20 | profile_pic 30 |

31 | @{main_thread?.user?.username} 32 |

33 |
34 |
35 |

{main_thread?.text}

36 |
37 |
38 |
39 | {replies.length > 1 && ( 40 |
41 | {replies.map((message, index) => { 42 | return ( 43 | 55 | ); 56 | })} 57 |
58 | )} 59 |
60 | ); 61 | }; 62 | 63 | export default page; 64 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/kshitijmohan15/threads-demon/c98d1cb150e83c8dbe050000c8636acb9280bcee/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 222.2 84% 4.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 222.2 84% 4.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 222.2 84% 4.9%; 15 | 16 | --primary: 222.2 47.4% 11.2%; 17 | --primary-foreground: 210 40% 98%; 18 | 19 | --secondary: 210 40% 96.1%; 20 | --secondary-foreground: 222.2 47.4% 11.2%; 21 | 22 | --muted: 210 40% 96.1%; 23 | --muted-foreground: 215.4 16.3% 46.9%; 24 | 25 | --accent: 210 40% 96.1%; 26 | --accent-foreground: 222.2 47.4% 11.2%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 210 40% 98%; 30 | 31 | --border: 214.3 31.8% 91.4%; 32 | --input: 214.3 31.8% 91.4%; 33 | --ring: 222.2 84% 4.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 222.2 84% 4.9%; 40 | --foreground: 210 40% 98%; 41 | 42 | --card: 222.2 84% 4.9%; 43 | --card-foreground: 210 40% 98%; 44 | 45 | --popover: 222.2 84% 4.9%; 46 | --popover-foreground: 210 40% 98%; 47 | 48 | --primary: 210 40% 98%; 49 | --primary-foreground: 222.2 47.4% 11.2%; 50 | 51 | --secondary: 217.2 32.6% 17.5%; 52 | --secondary-foreground: 210 40% 98%; 53 | 54 | --muted: 217.2 32.6% 17.5%; 55 | --muted-foreground: 215 20.2% 65.1%; 56 | 57 | --accent: 217.2 32.6% 17.5%; 58 | --accent-foreground: 210 40% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 210 40% 98%; 62 | 63 | --border: 217.2 32.6% 17.5%; 64 | --input: 217.2 32.6% 17.5%; 65 | --ring: hsl(212.7,26.8%,83.9); 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Sidebar } from "@/components/Sidebar"; 2 | import ClientToast from "@/components/client/ClientToast"; 3 | import { ThemeProvider } from "@/components/theme-provider"; 4 | import { ModeToggle } from "@/components/theme-toggle"; 5 | import type { Metadata } from "next"; 6 | import { Inter } from "next/font/google"; 7 | import "./globals.css"; 8 | import { AuthAlert } from "@/components/client/AuthAlert"; 9 | 10 | const inter = Inter({ subsets: ["latin"] }); 11 | 12 | export const metadata: Metadata = { 13 | title: "Create Next App", 14 | description: "Generated by create next app", 15 | }; 16 | 17 | export default function RootLayout({ 18 | children, 19 | }: { 20 | children: React.ReactNode; 21 | }) { 22 | return ( 23 | 24 | 25 | 30 |
31 |
32 | 33 |
38 | {children} 39 |
40 |
41 |
42 | 43 | 44 |
45 | 46 | 47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { AddThreadForm } from "@/components/client/AddThreadForm"; 2 | import { AuthAlert } from "@/components/client/AuthAlert"; 3 | import { Thread, User } from "@/db/schema"; 4 | import { getAuthSession } from "@/lib/auth"; 5 | import { getFeedAction } from "./_actions/thread"; 6 | import { ThreadList } from "@/components/client/ThreadList"; 7 | export default async function Home() { 8 | const session = await getAuthSession(); 9 | const thread_data = await getFeedAction(); 10 | return ( 11 |
12 | {/*
{JSON.stringify(session, null, 2)}
*/} 13 | 14 | 15 | 16 |
17 | ); 18 | } 19 | -------------------------------------------------------------------------------- /src/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { getAuthSession } from "@/lib/auth"; 2 | import SignIn from "./client/SignIn"; 3 | import { SignOut } from "./client/SignOut"; 4 | 5 | export const Sidebar = async () => { 6 | const session = await getAuthSession(); 7 | return ( 8 |
9 |
10 | {!session ? ( 11 | 12 | ) : ( 13 | 14 | )} 15 |
16 |
17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /src/components/client/AddThreadForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | import { addThreadsSchema } from "@/validations/thread"; 3 | import React, { useState } from "react"; 4 | import { useFieldArray, useForm } from "react-hook-form"; 5 | import { z } from "zod"; 6 | import { zodResolver } from "@hookform/resolvers/zod"; 7 | import { 8 | Form, 9 | FormControl, 10 | FormField, 11 | FormItem, 12 | FormMessage, 13 | } from "../ui/form"; 14 | import { Button } from "../ui/button"; 15 | import { motion } from "framer-motion"; 16 | import { v4 as uuidv4 } from "uuid"; 17 | import { Session } from "next-auth"; 18 | import Image from "next/image"; 19 | import { TextArea } from "./TextArea"; 20 | import { X } from "lucide-react"; 21 | import { catchError } from "@/lib/utils"; 22 | import { addThreadsAction } from "@/app/_actions/thread"; 23 | import { toast } from "sonner"; 24 | export const AddThreadForm = ({ 25 | session, 26 | }: { 27 | session: Session | undefined | null; 28 | }) => { 29 | async function onSubmit(data: z.infer) { 30 | try { 31 | await addThreadsAction(data); 32 | toast.success("Thread added successfully"); 33 | form.reset(); 34 | } catch (error) { 35 | catchError(error); 36 | } 37 | } 38 | const [dialogueId, _] = useState(uuidv4()); 39 | const form = useForm>({ 40 | resolver: zodResolver(addThreadsSchema), 41 | defaultValues: { 42 | threads: [{ text: "", dialogueId: dialogueId }], 43 | }, 44 | }); 45 | const { fields, append, remove } = useFieldArray({ 46 | name: "threads", 47 | control: form.control, 48 | }); 49 | return ( 50 |
51 | 55 | {/* {JSON.stringify(form.watch(), null, 2)} */} 56 |
57 | {fields.map((field, index) => { 58 | return ( 59 | 74 |
75 | ( 79 | 80 |
81 |
82 | profile_pic 93 |
94 |
95 |
96 | @ 97 | {session?.user.username} 98 |
99 | 100 |