├── .env.example
├── .eslintrc.json
├── .gitignore
├── README.md
├── architecture.excalidraw
├── drizzle.config.ts
├── next.config.mjs
├── package-lock.json
├── package.json
├── postcss.config.js
├── public
├── next.svg
└── vercel.svg
├── seed.ts
├── src
├── app
│ ├── api
│ │ └── webhooks
│ │ │ └── clerk
│ │ │ └── route.ts
│ ├── components
│ │ └── element.tsx
│ ├── config.ts
│ ├── db
│ │ ├── index.ts
│ │ ├── migrations
│ │ │ ├── 0000_red_proemial_gods.sql
│ │ │ ├── 0001_abandoned_rachel_grey.sql
│ │ │ ├── 0002_nappy_obadiah_stane.sql
│ │ │ ├── 0003_panoramic_marten_broadcloak.sql
│ │ │ ├── 0004_square_falcon.sql
│ │ │ └── meta
│ │ │ │ ├── 0000_snapshot.json
│ │ │ │ ├── 0001_snapshot.json
│ │ │ │ ├── 0002_snapshot.json
│ │ │ │ ├── 0003_snapshot.json
│ │ │ │ ├── 0004_snapshot.json
│ │ │ │ └── _journal.json
│ │ └── schema.ts
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.tsx
│ ├── log.ts
│ ├── page.tsx
│ └── voted
│ │ └── page.tsx
└── middleware.ts
├── tailwind.config.ts
└── tsconfig.json
/.env.example:
--------------------------------------------------------------------------------
1 | # This can be found on dashboard.clerk.com
2 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_replaceThisWithRealValues
3 | CLERK_SECRET_KEY=sk_test_replaceThisWithRealValues
4 |
5 | # Get your connection string from console.neon.tech
6 | DATABASE_URL='postgresql://neondb_owner:replacethis@ep-adj-noun-123245.us-east-1.aws.neon.tech/neondb?sslmode=require'
7 |
8 | # Can be found in the webhook endpoints section for
9 | # your application dashboard.clerk.com
10 | CLERK_WEBHOOK_SECRET='whsec_foobar'
11 |
12 | # Set log level to trace. Defaults to "info" if not explicitly defined
13 | LOG_LEVEL=trace
14 |
--------------------------------------------------------------------------------
/.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 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Neon, Clerk, and Vercel Example
2 |
3 | Companion repository for [this article on the Neon blog](https://neon.tech/blog/nextjs-authentication-using-clerk-drizzle-orm-and-neon).
4 |
5 | A live preview of this application with Discord login is available at https://neon-clerk-drizzle-nextjs.vercel.app/.
6 |
7 | A sample application that demonstrates how to use [Clerk](https://clerk.com/) authentication with [Next.js](https://nextjs.org/), and store user identifiers in [Neon's Serverless Postgres](https://neon.tech/github/) using [Drizzle ORM](https://orm.drizzle.team/).
8 |
9 | ## Local Development
10 |
11 | Requires Node.js 18.x.
12 |
13 | 1. Sign up to [Neon](https://neon.tech/github/) to access serverless Postgres by creating a project.
14 | 1. Sign up to [Clerk](https://clerk.com/) for user management and authentication. Create an application that supports sign in using a providers such as Discord and Google;
15 | 1. Clone this repository, install dependencies, and prepare a _.env.local_ file:
16 | ```bash
17 | git clone $REPO_URL neon-clerk-vercel
18 |
19 | cd neon-clerk-vercel
20 |
21 | npm install
22 |
23 | cp .env.example .env.local
24 | ```
25 | 1. Replace the values in _.env.local_ as follows:
26 | * `DATABASE_URL` - With your Neon [connection string](https://neon.tech/docs/connect/connect-from-any-app)`
27 | * `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` - With the value from the **API Keys** section in the Clerk dashboard.
28 | * `CLERK_SECRET_KEY` - With the value from the **API Keys** section in the Clerk dashboard.
29 | * `CLERK_WEBHOOK_SECRET` - This will be obtained later.
30 | 1. Generate and push the database schemas, and insert seed data:
31 | ```bash
32 | npm run drizzle:generate -- dotenv_config_path=.env.local
33 | npm run drizzle:push -- dotenv_config_path=.env.local
34 | npm run seed -- dotenv_config_path=.env.local
35 | ```
36 |
37 | > [!TIP]
38 | > Consider creating a separate [Neon database branch(es)](https://neon.tech/docs/manage/branches#create-a-branch) for your development environment(s).
39 |
40 | Since this application uses Clerk webhooks to create user references in the
41 | Neon Postgres database, you need a way to expose the application from your
42 | local network as a public HTTPS endpoint during local development. You can use
43 | [localtunnel](https://www.npmjs.com/package/localtunnel) to do this.
44 |
45 | 1. Open a terminal and start the Next.js application in development mode:
46 | ```bash
47 | npm run dev
48 | ```
49 | 1. Open another terminal and run the following command to obtain a public HTTPS URL to access your Next.js application:
50 | ```bash
51 | npx localtunnel@2.0 –port 3000 -s $USER
52 | ```
53 | 1. Go to [dashboard.clerk.com](https://dashboard.clerk.com) and select your application.
54 | 1. Navigate to the **Webhooks** screen.
55 | 1. Click the **Add Endpoint** button.
56 | 1. Enter the public HTTPS URL provided by localtunnel followed by _/api/webhooks/clerk_ in the **Endpoint URL** field.
57 | 1. Under the **Message Filtering** section select the user events.
58 | 1. Click the **Create** button.
59 |
60 | You can now visit http://localhost:3000/ to verify the application is working
61 | end-to-end. The application should display a login page, and once you've logged
62 | in your user's ID should be inserted into your Postgres users table a few
63 | seconds later.
64 |
65 | ## Vercel Deployment
66 |
67 | TODO: Vercel Deploy.
68 |
--------------------------------------------------------------------------------
/architecture.excalidraw:
--------------------------------------------------------------------------------
1 | {
2 | "type": "excalidraw",
3 | "version": 2,
4 | "source": "https://excalidraw.com",
5 | "elements": [
6 | {
7 | "type": "rectangle",
8 | "version": 654,
9 | "versionNonce": 142368455,
10 | "isDeleted": false,
11 | "id": "nZ9Dc0kjn1tiYxJuMsm93",
12 | "fillStyle": "hachure",
13 | "strokeWidth": 2,
14 | "strokeStyle": "solid",
15 | "roughness": 0,
16 | "opacity": 100,
17 | "angle": 0,
18 | "x": 2417.6148986435396,
19 | "y": 3174.080093534389,
20 | "strokeColor": "#1e1e1e",
21 | "backgroundColor": "#b2f2bb",
22 | "width": 258.01072980041806,
23 | "height": 237.5663362060164,
24 | "seed": 174392934,
25 | "groupIds": [],
26 | "frameId": null,
27 | "roundness": {
28 | "type": 3
29 | },
30 | "boundElements": [
31 | {
32 | "type": "text",
33 | "id": "7a2t7KDEbZs-iBNfgdn3g"
34 | },
35 | {
36 | "id": "0EMbyffG1-zhFoAMzUYps",
37 | "type": "arrow"
38 | },
39 | {
40 | "id": "jUkTh90tCmkw8ctSVMWRX",
41 | "type": "arrow"
42 | },
43 | {
44 | "id": "St3Es2_iIEC9dXxUJWcXM",
45 | "type": "arrow"
46 | }
47 | ],
48 | "updated": 1711494704492,
49 | "link": null,
50 | "locked": false
51 | },
52 | {
53 | "type": "text",
54 | "version": 332,
55 | "versionNonce": 724757991,
56 | "isDeleted": false,
57 | "id": "7a2t7KDEbZs-iBNfgdn3g",
58 | "fillStyle": "hachure",
59 | "strokeWidth": 2,
60 | "strokeStyle": "dashed",
61 | "roughness": 1,
62 | "opacity": 100,
63 | "angle": 0,
64 | "x": 2468.4282927185527,
65 | "y": 3247.8632616373975,
66 | "strokeColor": "#1e1e1e",
67 | "backgroundColor": "#d0bfff",
68 | "width": 156.38394165039062,
69 | "height": 90,
70 | "seed": 1447983590,
71 | "groupIds": [],
72 | "frameId": null,
73 | "roundness": null,
74 | "boundElements": [],
75 | "updated": 1711494704492,
76 | "link": null,
77 | "locked": false,
78 | "fontSize": 36,
79 | "fontFamily": 1,
80 | "text": "Neon\nPostgres",
81 | "textAlign": "center",
82 | "verticalAlign": "middle",
83 | "containerId": "nZ9Dc0kjn1tiYxJuMsm93",
84 | "originalText": "Neon\nPostgres",
85 | "lineHeight": 1.25
86 | },
87 | {
88 | "type": "rectangle",
89 | "version": 190,
90 | "versionNonce": 1635730247,
91 | "isDeleted": false,
92 | "id": "h8sLOWMaJE3LSdZzqvVNN",
93 | "fillStyle": "hachure",
94 | "strokeWidth": 1,
95 | "strokeStyle": "solid",
96 | "roughness": 1,
97 | "opacity": 100,
98 | "angle": 0,
99 | "x": 1851.1232253954709,
100 | "y": 3174.6818894380926,
101 | "strokeColor": "#1e1e1e",
102 | "backgroundColor": "#e9ecef",
103 | "width": 251.80565106752763,
104 | "height": 248.2749152703749,
105 | "seed": 2125596518,
106 | "groupIds": [],
107 | "frameId": null,
108 | "roundness": {
109 | "type": 3
110 | },
111 | "boundElements": [
112 | {
113 | "type": "text",
114 | "id": "gk_nw5VdK_G5eRb3dcprN"
115 | },
116 | {
117 | "id": "SJpr2doNnhQlrw6ElMFGB",
118 | "type": "arrow"
119 | },
120 | {
121 | "id": "3cdIc50uNGGvAbQUg08rE",
122 | "type": "arrow"
123 | },
124 | {
125 | "id": "jUkTh90tCmkw8ctSVMWRX",
126 | "type": "arrow"
127 | },
128 | {
129 | "id": "YYHlAuNxgcntiSweB4Arb",
130 | "type": "arrow"
131 | },
132 | {
133 | "id": "St3Es2_iIEC9dXxUJWcXM",
134 | "type": "arrow"
135 | }
136 | ],
137 | "updated": 1711494704492,
138 | "link": null,
139 | "locked": false
140 | },
141 | {
142 | "type": "text",
143 | "version": 122,
144 | "versionNonce": 459620967,
145 | "isDeleted": false,
146 | "id": "gk_nw5VdK_G5eRb3dcprN",
147 | "fillStyle": "hachure",
148 | "strokeWidth": 2,
149 | "strokeStyle": "dashed",
150 | "roughness": 1,
151 | "opacity": 100,
152 | "angle": 0,
153 | "x": 1878.3140758315785,
154 | "y": 3253.81934707328,
155 | "strokeColor": "#1e1e1e",
156 | "backgroundColor": "#d0bfff",
157 | "width": 197.4239501953125,
158 | "height": 90,
159 | "seed": 619452774,
160 | "groupIds": [],
161 | "frameId": null,
162 | "roundness": null,
163 | "boundElements": [],
164 | "updated": 1711494704492,
165 | "link": null,
166 | "locked": false,
167 | "fontSize": 36,
168 | "fontFamily": 1,
169 | "text": "Next.js on \nVercel",
170 | "textAlign": "center",
171 | "verticalAlign": "middle",
172 | "containerId": "h8sLOWMaJE3LSdZzqvVNN",
173 | "originalText": "Next.js on Vercel",
174 | "lineHeight": 1.25
175 | },
176 | {
177 | "type": "rectangle",
178 | "version": 420,
179 | "versionNonce": 1922042151,
180 | "isDeleted": false,
181 | "id": "Np0ajYvtlgJWKQ_mrYrNL",
182 | "fillStyle": "hachure",
183 | "strokeWidth": 1,
184 | "strokeStyle": "solid",
185 | "roughness": 1,
186 | "opacity": 100,
187 | "angle": 0,
188 | "x": 1849.3993493742587,
189 | "y": 3597.370607100792,
190 | "strokeColor": "#1e1e1e",
191 | "backgroundColor": "#d0bfff",
192 | "width": 259.57274582100064,
193 | "height": 233.04274778979288,
194 | "seed": 1117400250,
195 | "groupIds": [],
196 | "frameId": null,
197 | "roundness": {
198 | "type": 3
199 | },
200 | "boundElements": [
201 | {
202 | "type": "text",
203 | "id": "ZFhMNueSCNj1EJQK1xF5W"
204 | },
205 | {
206 | "id": "3cdIc50uNGGvAbQUg08rE",
207 | "type": "arrow"
208 | },
209 | {
210 | "id": "7CDx8KJpN8Ou9RxUHj9eT",
211 | "type": "arrow"
212 | }
213 | ],
214 | "updated": 1711494704492,
215 | "link": null,
216 | "locked": false
217 | },
218 | {
219 | "type": "text",
220 | "version": 280,
221 | "versionNonce": 1042435143,
222 | "isDeleted": false,
223 | "id": "ZFhMNueSCNj1EJQK1xF5W",
224 | "fillStyle": "hachure",
225 | "strokeWidth": 2,
226 | "strokeStyle": "dashed",
227 | "roughness": 1,
228 | "opacity": 100,
229 | "angle": 0,
230 | "x": 1936.5077437691339,
231 | "y": 3668.8919809956883,
232 | "strokeColor": "#1e1e1e",
233 | "backgroundColor": "#d0bfff",
234 | "width": 85.35595703125,
235 | "height": 90,
236 | "seed": 282478458,
237 | "groupIds": [],
238 | "frameId": null,
239 | "roundness": null,
240 | "boundElements": [],
241 | "updated": 1711494704492,
242 | "link": null,
243 | "locked": false,
244 | "fontSize": 36,
245 | "fontFamily": 1,
246 | "text": "Clerk\nAuth",
247 | "textAlign": "center",
248 | "verticalAlign": "middle",
249 | "containerId": "Np0ajYvtlgJWKQ_mrYrNL",
250 | "originalText": "Clerk\nAuth",
251 | "lineHeight": 1.25
252 | },
253 | {
254 | "type": "ellipse",
255 | "version": 241,
256 | "versionNonce": 1589296551,
257 | "isDeleted": false,
258 | "id": "tP2NM8fHquJpLRWGNpeMS",
259 | "fillStyle": "hachure",
260 | "strokeWidth": 1,
261 | "strokeStyle": "solid",
262 | "roughness": 1,
263 | "opacity": 100,
264 | "angle": 0,
265 | "x": 1330.1102812947918,
266 | "y": 3214.5986394783204,
267 | "strokeColor": "#1e1e1e",
268 | "backgroundColor": "#a5d8ff",
269 | "width": 150.05935887249228,
270 | "height": 143.96763158601922,
271 | "seed": 1965001914,
272 | "groupIds": [],
273 | "frameId": null,
274 | "roundness": {
275 | "type": 2
276 | },
277 | "boundElements": [
278 | {
279 | "type": "text",
280 | "id": "uoNQSQUrr4aasR7MTUUn1"
281 | },
282 | {
283 | "id": "SJpr2doNnhQlrw6ElMFGB",
284 | "type": "arrow"
285 | },
286 | {
287 | "id": "YYHlAuNxgcntiSweB4Arb",
288 | "type": "arrow"
289 | },
290 | {
291 | "id": "7CDx8KJpN8Ou9RxUHj9eT",
292 | "type": "arrow"
293 | }
294 | ],
295 | "updated": 1711494704492,
296 | "link": null,
297 | "locked": false
298 | },
299 | {
300 | "type": "text",
301 | "version": 182,
302 | "versionNonce": 738357447,
303 | "isDeleted": false,
304 | "id": "uoNQSQUrr4aasR7MTUUn1",
305 | "fillStyle": "hachure",
306 | "strokeWidth": 2,
307 | "strokeStyle": "dashed",
308 | "roughness": 1,
309 | "opacity": 100,
310 | "angle": 0,
311 | "x": 1364.8739828843645,
312 | "y": 3264.1822109884097,
313 | "strokeColor": "#1e1e1e",
314 | "backgroundColor": "#a5d8ff",
315 | "width": 80.42396545410156,
316 | "height": 45,
317 | "seed": 1446013370,
318 | "groupIds": [],
319 | "frameId": null,
320 | "roundness": null,
321 | "boundElements": [],
322 | "updated": 1711494704492,
323 | "link": null,
324 | "locked": false,
325 | "fontSize": 36,
326 | "fontFamily": 1,
327 | "text": "User",
328 | "textAlign": "center",
329 | "verticalAlign": "middle",
330 | "containerId": "tP2NM8fHquJpLRWGNpeMS",
331 | "originalText": "User",
332 | "lineHeight": 1.25
333 | },
334 | {
335 | "type": "arrow",
336 | "version": 382,
337 | "versionNonce": 396436489,
338 | "isDeleted": false,
339 | "id": "SJpr2doNnhQlrw6ElMFGB",
340 | "fillStyle": "hachure",
341 | "strokeWidth": 2,
342 | "strokeStyle": "dashed",
343 | "roughness": 1,
344 | "opacity": 100,
345 | "angle": 0,
346 | "x": 1490.7083358573677,
347 | "y": 3250.899189145555,
348 | "strokeColor": "#1e1e1e",
349 | "backgroundColor": "#a5d8ff",
350 | "width": 340.56128709269865,
351 | "height": 0.08626370090769342,
352 | "seed": 1520036262,
353 | "groupIds": [],
354 | "frameId": null,
355 | "roundness": {
356 | "type": 2
357 | },
358 | "boundElements": [],
359 | "updated": 1711494704504,
360 | "link": null,
361 | "locked": false,
362 | "startBinding": {
363 | "elementId": "tP2NM8fHquJpLRWGNpeMS",
364 | "focus": -0.49541122736371607,
365 | "gap": 18.149915410486457
366 | },
367 | "endBinding": {
368 | "elementId": "h8sLOWMaJE3LSdZzqvVNN",
369 | "focus": 0.386917883143017,
370 | "gap": 19.853602445404476
371 | },
372 | "lastCommittedPoint": null,
373 | "startArrowhead": null,
374 | "endArrowhead": "arrow",
375 | "points": [
376 | [
377 | 0,
378 | 0
379 | ],
380 | [
381 | 340.56128709269865,
382 | -0.08626370090769342
383 | ]
384 | ]
385 | },
386 | {
387 | "type": "arrow",
388 | "version": 638,
389 | "versionNonce": 730625481,
390 | "isDeleted": false,
391 | "id": "YYHlAuNxgcntiSweB4Arb",
392 | "fillStyle": "hachure",
393 | "strokeWidth": 2,
394 | "strokeStyle": "dashed",
395 | "roughness": 1,
396 | "opacity": 100,
397 | "angle": 0,
398 | "x": 1487.0835414163787,
399 | "y": 3343.7016770617097,
400 | "strokeColor": "#1e1e1e",
401 | "backgroundColor": "#a5d8ff",
402 | "width": 339.3101798930629,
403 | "height": 0.32080820194778426,
404 | "seed": 1602743738,
405 | "groupIds": [],
406 | "frameId": null,
407 | "roundness": {
408 | "type": 2
409 | },
410 | "boundElements": [],
411 | "updated": 1711494704504,
412 | "link": null,
413 | "locked": false,
414 | "startBinding": {
415 | "elementId": "tP2NM8fHquJpLRWGNpeMS",
416 | "focus": 0.792424215358263,
417 | "gap": 25.880398905556888
418 | },
419 | "endBinding": {
420 | "elementId": "h8sLOWMaJE3LSdZzqvVNN",
421 | "focus": -0.36493510245487293,
422 | "gap": 24.729504086029237
423 | },
424 | "lastCommittedPoint": null,
425 | "startArrowhead": null,
426 | "endArrowhead": "arrow",
427 | "points": [
428 | [
429 | 0,
430 | 0
431 | ],
432 | [
433 | 339.3101798930629,
434 | 0.32080820194778426
435 | ]
436 | ]
437 | },
438 | {
439 | "type": "text",
440 | "version": 183,
441 | "versionNonce": 1657728903,
442 | "isDeleted": false,
443 | "id": "uDeF8jPXtfFMd4JCIzotR",
444 | "fillStyle": "hachure",
445 | "strokeWidth": 2,
446 | "strokeStyle": "solid",
447 | "roughness": 1,
448 | "opacity": 100,
449 | "angle": 0,
450 | "x": 1572.225599257121,
451 | "y": 3210.7138953142353,
452 | "strokeColor": "#1e1e1e",
453 | "backgroundColor": "#ffffff",
454 | "width": 156.73988342285156,
455 | "height": 25,
456 | "seed": 926156090,
457 | "groupIds": [],
458 | "frameId": null,
459 | "roundness": null,
460 | "boundElements": [],
461 | "updated": 1711494704492,
462 | "link": null,
463 | "locked": false,
464 | "fontSize": 20,
465 | "fontFamily": 1,
466 | "text": "Sign up / Sign in",
467 | "textAlign": "center",
468 | "verticalAlign": "top",
469 | "containerId": null,
470 | "originalText": "Sign up / Sign in",
471 | "lineHeight": 1.25
472 | },
473 | {
474 | "type": "text",
475 | "version": 446,
476 | "versionNonce": 1718970023,
477 | "isDeleted": false,
478 | "id": "T8smpxHWB6lNzxWh42-FB",
479 | "fillStyle": "hachure",
480 | "strokeWidth": 2,
481 | "strokeStyle": "solid",
482 | "roughness": 1,
483 | "opacity": 100,
484 | "angle": 0,
485 | "x": 1526.39221258768,
486 | "y": 3302.236339593994,
487 | "strokeColor": "#1e1e1e",
488 | "backgroundColor": "#ffffff",
489 | "width": 239.3597869873047,
490 | "height": 25,
491 | "seed": 1873248998,
492 | "groupIds": [],
493 | "frameId": null,
494 | "roundness": null,
495 | "boundElements": [],
496 | "updated": 1711494704492,
497 | "link": null,
498 | "locked": false,
499 | "fontSize": 20,
500 | "fontFamily": 1,
501 | "text": "Authenticated Requests",
502 | "textAlign": "center",
503 | "verticalAlign": "top",
504 | "containerId": null,
505 | "originalText": "Authenticated Requests",
506 | "lineHeight": 1.25
507 | },
508 | {
509 | "type": "arrow",
510 | "version": 259,
511 | "versionNonce": 446584713,
512 | "isDeleted": false,
513 | "id": "3cdIc50uNGGvAbQUg08rE",
514 | "fillStyle": "hachure",
515 | "strokeWidth": 2,
516 | "strokeStyle": "dashed",
517 | "roughness": 1,
518 | "opacity": 100,
519 | "angle": 0,
520 | "x": 1976.937417171494,
521 | "y": 3435.1943878287966,
522 | "strokeColor": "#1e1e1e",
523 | "backgroundColor": "#a5d8ff",
524 | "width": 2.75487179263655,
525 | "height": 143.70549926124932,
526 | "seed": 1791264934,
527 | "groupIds": [],
528 | "frameId": null,
529 | "roundness": {
530 | "type": 2
531 | },
532 | "boundElements": [],
533 | "updated": 1711494704504,
534 | "link": null,
535 | "locked": false,
536 | "startBinding": {
537 | "elementId": "h8sLOWMaJE3LSdZzqvVNN",
538 | "focus": 0.02107050625663146,
539 | "gap": 12.237583120328736
540 | },
541 | "endBinding": {
542 | "elementId": "Np0ajYvtlgJWKQ_mrYrNL",
543 | "focus": 0.02343885366403433,
544 | "gap": 18.470720010745936
545 | },
546 | "lastCommittedPoint": null,
547 | "startArrowhead": null,
548 | "endArrowhead": "arrow",
549 | "points": [
550 | [
551 | 0,
552 | 0
553 | ],
554 | [
555 | 2.75487179263655,
556 | 143.70549926124932
557 | ]
558 | ]
559 | },
560 | {
561 | "type": "text",
562 | "version": 292,
563 | "versionNonce": 1309354215,
564 | "isDeleted": false,
565 | "id": "ud7QNKJknjMYaVOyF3BWJ",
566 | "fillStyle": "hachure",
567 | "strokeWidth": 2,
568 | "strokeStyle": "solid",
569 | "roughness": 1,
570 | "opacity": 100,
571 | "angle": 0,
572 | "x": 1818.222264317722,
573 | "y": 3481.721039389629,
574 | "strokeColor": "#1e1e1e",
575 | "backgroundColor": "#a5d8ff",
576 | "width": 146.73988342285156,
577 | "height": 50,
578 | "seed": 1985492602,
579 | "groupIds": [],
580 | "frameId": null,
581 | "roundness": null,
582 | "boundElements": [],
583 | "updated": 1711494704492,
584 | "link": null,
585 | "locked": false,
586 | "fontSize": 20,
587 | "fontFamily": 1,
588 | "text": "Sign up/ Sign in\nRedirect",
589 | "textAlign": "center",
590 | "verticalAlign": "top",
591 | "containerId": null,
592 | "originalText": "Sign up/ Sign in\nRedirect",
593 | "lineHeight": 1.25
594 | },
595 | {
596 | "type": "arrow",
597 | "version": 755,
598 | "versionNonce": 410175817,
599 | "isDeleted": false,
600 | "id": "jUkTh90tCmkw8ctSVMWRX",
601 | "fillStyle": "hachure",
602 | "strokeWidth": 2,
603 | "strokeStyle": "dashed",
604 | "roughness": 1,
605 | "opacity": 100,
606 | "angle": 0,
607 | "x": 2119.9597408889745,
608 | "y": 3265.462383867956,
609 | "strokeColor": "#1e1e1e",
610 | "backgroundColor": "#a5d8ff",
611 | "width": 280.68673453930455,
612 | "height": 1.8206189199577238,
613 | "seed": 511234746,
614 | "groupIds": [],
615 | "frameId": null,
616 | "roundness": {
617 | "type": 2
618 | },
619 | "boundElements": [],
620 | "updated": 1711494704504,
621 | "link": null,
622 | "locked": false,
623 | "startBinding": {
624 | "elementId": "h8sLOWMaJE3LSdZzqvVNN",
625 | "focus": -0.27437334110380135,
626 | "gap": 17.03086442597578
627 | },
628 | "endBinding": {
629 | "elementId": "nZ9Dc0kjn1tiYxJuMsm93",
630 | "focus": 0.2059307974328775,
631 | "gap": 16.9684232152606
632 | },
633 | "lastCommittedPoint": null,
634 | "startArrowhead": null,
635 | "endArrowhead": "arrow",
636 | "points": [
637 | [
638 | 0,
639 | 0
640 | ],
641 | [
642 | 280.68673453930455,
643 | 1.8206189199577238
644 | ]
645 | ]
646 | },
647 | {
648 | "type": "arrow",
649 | "version": 806,
650 | "versionNonce": 564513545,
651 | "isDeleted": false,
652 | "id": "St3Es2_iIEC9dXxUJWcXM",
653 | "fillStyle": "hachure",
654 | "strokeWidth": 2,
655 | "strokeStyle": "dashed",
656 | "roughness": 1,
657 | "opacity": 100,
658 | "angle": 6.272666212319525,
659 | "x": 2121.689771848503,
660 | "y": 3346.996491096031,
661 | "strokeColor": "#1e1e1e",
662 | "backgroundColor": "#a5d8ff",
663 | "width": 280.67057918006617,
664 | "height": 5.394493314251122,
665 | "seed": 1679256634,
666 | "groupIds": [],
667 | "frameId": null,
668 | "roundness": {
669 | "type": 2
670 | },
671 | "boundElements": [],
672 | "updated": 1711494704504,
673 | "link": null,
674 | "locked": false,
675 | "startBinding": {
676 | "elementId": "h8sLOWMaJE3LSdZzqvVNN",
677 | "focus": 0.3865094596104668,
678 | "gap": 18.734656926715616
679 | },
680 | "endBinding": {
681 | "elementId": "nZ9Dc0kjn1tiYxJuMsm93",
682 | "focus": -0.4946773533616025,
683 | "gap": 15.239570080344265
684 | },
685 | "lastCommittedPoint": null,
686 | "startArrowhead": null,
687 | "endArrowhead": "arrow",
688 | "points": [
689 | [
690 | 0,
691 | 0
692 | ],
693 | [
694 | 280.67057918006617,
695 | 5.394493314251122
696 | ]
697 | ]
698 | },
699 | {
700 | "type": "text",
701 | "version": 454,
702 | "versionNonce": 559767111,
703 | "isDeleted": false,
704 | "id": "gDoLHxn2KiXzeIWta779Z",
705 | "fillStyle": "hachure",
706 | "strokeWidth": 2,
707 | "strokeStyle": "solid",
708 | "roughness": 1,
709 | "opacity": 100,
710 | "angle": 0,
711 | "x": 2169.1697068795775,
712 | "y": 3231.5305543286167,
713 | "strokeColor": "#1e1e1e",
714 | "backgroundColor": "#a5d8ff",
715 | "width": 136.77987670898438,
716 | "height": 25,
717 | "seed": 245374438,
718 | "groupIds": [],
719 | "frameId": null,
720 | "roundness": null,
721 | "boundElements": [],
722 | "updated": 1711494704492,
723 | "link": null,
724 | "locked": false,
725 | "fontSize": 20,
726 | "fontFamily": 1,
727 | "text": "Create Quote",
728 | "textAlign": "center",
729 | "verticalAlign": "top",
730 | "containerId": null,
731 | "originalText": "Create Quote",
732 | "lineHeight": 1.25
733 | },
734 | {
735 | "type": "text",
736 | "version": 501,
737 | "versionNonce": 247987559,
738 | "isDeleted": false,
739 | "id": "2dEBJEHwoMAh4UoKjGnTj",
740 | "fillStyle": "hachure",
741 | "strokeWidth": 2,
742 | "strokeStyle": "solid",
743 | "roughness": 1,
744 | "opacity": 100,
745 | "angle": 6.272666212319525,
746 | "x": 2171.0969056322097,
747 | "y": 3313.0982258109248,
748 | "strokeColor": "#1e1e1e",
749 | "backgroundColor": "#a5d8ff",
750 | "width": 133.75987243652344,
751 | "height": 25,
752 | "seed": 479202406,
753 | "groupIds": [],
754 | "frameId": null,
755 | "roundness": null,
756 | "boundElements": [],
757 | "updated": 1711494704492,
758 | "link": null,
759 | "locked": false,
760 | "fontSize": 20,
761 | "fontFamily": 1,
762 | "text": "Delete Quote",
763 | "textAlign": "center",
764 | "verticalAlign": "top",
765 | "containerId": null,
766 | "originalText": "Delete Quote",
767 | "lineHeight": 1.25
768 | },
769 | {
770 | "type": "arrow",
771 | "version": 267,
772 | "versionNonce": 1003707593,
773 | "isDeleted": false,
774 | "id": "7CDx8KJpN8Ou9RxUHj9eT",
775 | "fillStyle": "hachure",
776 | "strokeWidth": 2,
777 | "strokeStyle": "dashed",
778 | "roughness": 1,
779 | "opacity": 100,
780 | "angle": 0,
781 | "x": 1837.8267704161258,
782 | "y": 3719.382993470732,
783 | "strokeColor": "#1e1e1e",
784 | "backgroundColor": "#a5d8ff",
785 | "width": 434.55307786686444,
786 | "height": 355.41154539207855,
787 | "seed": 628524646,
788 | "groupIds": [],
789 | "frameId": null,
790 | "roundness": {
791 | "type": 2
792 | },
793 | "boundElements": [],
794 | "updated": 1711494704504,
795 | "link": null,
796 | "locked": false,
797 | "startBinding": {
798 | "elementId": "Np0ajYvtlgJWKQ_mrYrNL",
799 | "focus": -0.013284723008534132,
800 | "gap": 11.572578958132908
801 | },
802 | "endBinding": {
803 | "elementId": "tP2NM8fHquJpLRWGNpeMS",
804 | "focus": -0.030252263779424575,
805 | "gap": 17.39133940928083
806 | },
807 | "lastCommittedPoint": null,
808 | "startArrowhead": null,
809 | "endArrowhead": "arrow",
810 | "points": [
811 | [
812 | 0,
813 | 0
814 | ],
815 | [
816 | -434.55307786686444,
817 | 11.975333116320144
818 | ],
819 | [
820 | -431.2479418181897,
821 | -343.4362122757584
822 | ]
823 | ]
824 | },
825 | {
826 | "type": "text",
827 | "version": 535,
828 | "versionNonce": 2050428839,
829 | "isDeleted": false,
830 | "id": "xwIWM3L3vwWcYoskQzDre",
831 | "fillStyle": "hachure",
832 | "strokeWidth": 2,
833 | "strokeStyle": "solid",
834 | "roughness": 1,
835 | "opacity": 100,
836 | "angle": 0,
837 | "x": 1422.2823151606128,
838 | "y": 3666.478254105988,
839 | "strokeColor": "#1e1e1e",
840 | "backgroundColor": "#a5d8ff",
841 | "width": 268.27978515625,
842 | "height": 50,
843 | "seed": 2018426874,
844 | "groupIds": [],
845 | "frameId": null,
846 | "roundness": null,
847 | "boundElements": [],
848 | "updated": 1711494704492,
849 | "link": null,
850 | "locked": false,
851 | "fontSize": 20,
852 | "fontFamily": 1,
853 | "text": "Redirect Authenticated\nUser to Next.js Appliaction",
854 | "textAlign": "center",
855 | "verticalAlign": "top",
856 | "containerId": null,
857 | "originalText": "Redirect Authenticated\nUser to Next.js Appliaction",
858 | "lineHeight": 1.25
859 | }
860 | ],
861 | "appState": {
862 | "gridSize": null,
863 | "viewBackgroundColor": "#ffffff"
864 | },
865 | "files": {}
866 | }
--------------------------------------------------------------------------------
/drizzle.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from 'drizzle-kit';
2 | import * as dotenv from "dotenv";
3 |
4 | dotenv.config();
5 |
6 | if (!('DATABASE_URL' in process.env)) throw new Error('DATABASE_URL not found in environment');
7 |
8 | export default {
9 | schema: './src/app/db/schema.ts',
10 | out: './src/app/db/migrations',
11 | driver: 'pg',
12 | dbCredentials: {
13 | connectionString: process.env.DATABASE_URL!,
14 | }
15 | } satisfies Config;
16 |
--------------------------------------------------------------------------------
/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "neon-clerk-discord",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "drizzle:generate": "drizzle-kit generate:pg",
8 | "drizzle:push": "drizzle-kit push:pg",
9 | "build": "next build",
10 | "start": "next start",
11 | "lint": "next lint",
12 | "seed": "tsx --require dotenv/config seed.ts"
13 | },
14 | "dependencies": {
15 | "@clerk/nextjs": "^4.29.9",
16 | "@clerk/types": "^3.62.1",
17 | "@neondatabase/serverless": "^0.9.0",
18 | "drizzle-orm": "^0.30.2",
19 | "env-var": "^7.4.1",
20 | "next": "14.1.3",
21 | "pino": "^8.19.0",
22 | "react": "^18",
23 | "react-dom": "^18",
24 | "svix": "^1.21.0"
25 | },
26 | "devDependencies": {
27 | "@types/node": "^20",
28 | "@types/react": "^18",
29 | "@types/react-dom": "^18",
30 | "autoprefixer": "^10.0.1",
31 | "dotenv": "^16.4.5",
32 | "drizzle-kit": "^0.20.14",
33 | "eslint": "^8",
34 | "eslint-config-next": "14.1.3",
35 | "postcss": "^8",
36 | "tailwindcss": "^3.3.0",
37 | "tsx": "^4.7.1",
38 | "typescript": "^5"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/postcss.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | };
7 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/seed.ts:
--------------------------------------------------------------------------------
1 | import { db } from "./src/app/db"
2 | import { Elements } from "./src/app/db/schema"
3 |
4 | type ElementData = {
5 | elements: {
6 | name: string
7 | number: number
8 | symbol: string
9 | }[]
10 | }
11 |
12 | async function main () {
13 | console.log('Downloading seed data...')
14 | const res = await fetch('https://raw.githubusercontent.com/Bowserinator/Periodic-Table-JSON/master/PeriodicTableJSON.json')
15 |
16 | if (res.status !== 200) {
17 | throw new Error(`received status code ${res.status} when fetching elements JSON`)
18 | }
19 | console.log('Parsing seed data...')
20 | const elementJson = await res.json() as ElementData
21 |
22 | const values = elementJson.elements.map(el => {
23 | const { symbol, name, number } = el
24 |
25 | return {
26 | symbol, name, atomicNumber: number
27 | }
28 | })
29 |
30 | console.log('Delete existing element data...')
31 | await db.delete(Elements)
32 | console.log('Insert new element data...')
33 | await db.insert(Elements).values(values)
34 | }
35 |
36 | main()
37 |
--------------------------------------------------------------------------------
/src/app/api/webhooks/clerk/route.ts:
--------------------------------------------------------------------------------
1 | import { WebhookEvent } from '@clerk/nextjs/server'
2 | import { NextResponse } from 'next/server'
3 | import { db } from '@/app/db'
4 | import { headers } from 'next/headers'
5 | import { Webhook } from 'svix'
6 | import { Users } from '@/app/db/schema'
7 | import { log } from '@/app/log'
8 | import { eq } from 'drizzle-orm'
9 |
10 | async function validateRequest(request: Request, secret: string) {
11 | const payloadString = await request.text()
12 | const headerPayload = headers()
13 |
14 | const svixHeaders = {
15 | 'svix-id': headerPayload.get('svix-id')!,
16 | 'svix-timestamp': headerPayload.get('svix-timestamp')!,
17 | 'svix-signature': headerPayload.get('svix-signature')!,
18 | }
19 | const wh = new Webhook(secret)
20 |
21 | try {
22 | return wh.verify(payloadString, svixHeaders) as WebhookEvent
23 | } catch (e) {
24 | console.error('incoming webhook failed verification')
25 | return
26 | }
27 | }
28 |
29 | export async function POST(req: Request): Promise {
30 | if (!process.env.CLERK_WEBHOOK_SECRET) {
31 | throw new Error('CLERK_WEBHOOK_SECRET environment variable is missing')
32 | }
33 |
34 | const payload = await validateRequest(req, process.env.CLERK_WEBHOOK_SECRET)
35 |
36 | if (!payload) {
37 | return NextResponse.json(
38 | { error: 'webhook verification failed or payload was malformed' },
39 | { status: 400 }
40 | )
41 | }
42 |
43 | const { type, data } = payload
44 |
45 | log.trace(`clerk webhook payload: ${{ data, type }}`)
46 |
47 | if (type === 'user.created') {
48 | return createUser(data.id, data.created_at)
49 | } else if (type === 'user.deleted') {
50 | return deleteUser(data.id)
51 | } else {
52 | log.warn(`${req.url} received event type "${type}", but no handler is defined for this type`)
53 | return NextResponse.json({
54 | error: `uncreognised payload type: ${type}`
55 | }, {
56 | status: 400
57 | })
58 | }
59 | }
60 |
61 | async function createUser(id: string, createdAt: number) {
62 | log.info('creating user due to clerk webhook')
63 | await db.insert(Users).values({
64 | id,
65 | clerkCreateTs: new Date(createdAt)
66 | })
67 |
68 | return NextResponse.json({
69 | message: 'user created'
70 | }, { status: 200 })
71 | }
72 |
73 | async function deleteUser(id?: string) {
74 | if (id) {
75 | log.info('delete user due to clerk webhook')
76 | await db.delete(Users).where(
77 | eq(Users.id, id)
78 | )
79 |
80 | return NextResponse.json({
81 | message: 'user deleted'
82 | }, { status: 200 })
83 | } else {
84 | log.warn('clerk sent a delete user request, but no user ID was included in the payload')
85 | return NextResponse.json({
86 | message: 'ok'
87 | }, { status: 200 })
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/app/components/element.tsx:
--------------------------------------------------------------------------------
1 | import { Element } from "../db/schema"
2 |
3 | export function ElementComponent (props: { name: string, symbol: string, atomicNumber: number, children: any }) {
4 | const { atomicNumber, name, symbol, children } = props
5 |
6 | return (
7 |
8 |
9 | {atomicNumber}
10 |
11 | {symbol}
12 | {name}
13 | {children}
14 |
15 | )
16 | }
17 |
--------------------------------------------------------------------------------
/src/app/config.ts:
--------------------------------------------------------------------------------
1 |
2 | import { from } from 'env-var'
3 |
4 | export default function getEnv (env: NodeJS.ProcessEnv) {
5 | const { get } = from(env)
6 |
7 | return {
8 | WEBHOOK_SECRET: get('WEBHOOK_SECRET').required().asString()
9 | }
10 | }
11 |
--------------------------------------------------------------------------------
/src/app/db/index.ts:
--------------------------------------------------------------------------------
1 | import { neon } from '@neondatabase/serverless';
2 | import { drizzle } from 'drizzle-orm/neon-http';
3 |
4 | // Redefining generic fixes a type error. Fix coming soon:
5 | // https://github.com/drizzle-team/drizzle-orm/issues/1945#event-12152755813
6 | const sql = neon(process.env.DATABASE_URL!);
7 | export const db = drizzle(sql);
8 |
--------------------------------------------------------------------------------
/src/app/db/migrations/0000_red_proemial_gods.sql:
--------------------------------------------------------------------------------
1 | CREATE TABLE IF NOT EXISTS "element_votes" (
2 | "element_id" integer,
3 | "user_id" text NOT NULL,
4 | "create_ts" timestamp DEFAULT now(),
5 | CONSTRAINT "element_votes_user_id_unique" UNIQUE("user_id")
6 | );
7 | --> statement-breakpoint
8 | CREATE TABLE IF NOT EXISTS "element" (
9 | "name" text NOT NULL,
10 | "symbol" varchar(3) NOT NULL,
11 | "atomicNumber" integer PRIMARY KEY NOT NULL
12 | );
13 | --> statement-breakpoint
14 | CREATE TABLE IF NOT EXISTS "users" (
15 | "id" text PRIMARY KEY NOT NULL
16 | );
17 | --> statement-breakpoint
18 | DO $$ BEGIN
19 | ALTER TABLE "element_votes" ADD CONSTRAINT "element_votes_element_id_element_atomicNumber_fk" FOREIGN KEY ("element_id") REFERENCES "element"("atomicNumber") ON DELETE no action ON UPDATE no action;
20 | EXCEPTION
21 | WHEN duplicate_object THEN null;
22 | END $$;
23 | --> statement-breakpoint
24 | DO $$ BEGIN
25 | ALTER TABLE "element_votes" ADD CONSTRAINT "element_votes_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE no action ON UPDATE no action;
26 | EXCEPTION
27 | WHEN duplicate_object THEN null;
28 | END $$;
29 |
--------------------------------------------------------------------------------
/src/app/db/migrations/0001_abandoned_rachel_grey.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "element_votes" ALTER COLUMN "element_id" SET NOT NULL;--> statement-breakpoint
2 | ALTER TABLE "element_votes" ALTER COLUMN "create_ts" SET NOT NULL;--> statement-breakpoint
3 | ALTER TABLE "users" ADD COLUMN "create_ts" timestamp DEFAULT now() NOT NULL;--> statement-breakpoint
4 | ALTER TABLE "users" ADD COLUMN "clerk_create_ts" timestamp NOT NULL;
--------------------------------------------------------------------------------
/src/app/db/migrations/0002_nappy_obadiah_stane.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "element_votes" DROP CONSTRAINT "element_votes_element_id_element_atomicNumber_fk";
2 | --> statement-breakpoint
3 | DO $$ BEGIN
4 | ALTER TABLE "element_votes" ADD CONSTRAINT "element_votes_element_id_element_atomicNumber_fk" FOREIGN KEY ("element_id") REFERENCES "element"("atomicNumber") ON DELETE cascade ON UPDATE no action;
5 | EXCEPTION
6 | WHEN duplicate_object THEN null;
7 | END $$;
8 |
--------------------------------------------------------------------------------
/src/app/db/migrations/0003_panoramic_marten_broadcloak.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "element_votes" DROP CONSTRAINT "element_votes_element_id_element_atomicNumber_fk";
2 | --> statement-breakpoint
3 | DO $$ BEGIN
4 | ALTER TABLE "element_votes" ADD CONSTRAINT "element_votes_element_id_element_atomicNumber_fk" FOREIGN KEY ("element_id") REFERENCES "element"("atomicNumber") ON DELETE no action ON UPDATE no action;
5 | EXCEPTION
6 | WHEN duplicate_object THEN null;
7 | END $$;
8 |
--------------------------------------------------------------------------------
/src/app/db/migrations/0004_square_falcon.sql:
--------------------------------------------------------------------------------
1 | ALTER TABLE "element" RENAME COLUMN "atomicNumber" TO "atomic_number";--> statement-breakpoint
2 | ALTER TABLE "element_votes" DROP CONSTRAINT "element_votes_element_id_element_atomicNumber_fk";
3 | --> statement-breakpoint
4 | DO $$ BEGIN
5 | ALTER TABLE "element_votes" ADD CONSTRAINT "element_votes_element_id_element_atomic_number_fk" FOREIGN KEY ("element_id") REFERENCES "element"("atomic_number") ON DELETE no action ON UPDATE no action;
6 | EXCEPTION
7 | WHEN duplicate_object THEN null;
8 | END $$;
9 |
--------------------------------------------------------------------------------
/src/app/db/migrations/meta/0000_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "fc858a9a-ca71-4f1c-9956-806ad458fb6f",
3 | "prevId": "00000000-0000-0000-0000-000000000000",
4 | "version": "5",
5 | "dialect": "pg",
6 | "tables": {
7 | "element_votes": {
8 | "name": "element_votes",
9 | "schema": "",
10 | "columns": {
11 | "element_id": {
12 | "name": "element_id",
13 | "type": "integer",
14 | "primaryKey": false,
15 | "notNull": false
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "create_ts": {
24 | "name": "create_ts",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": false,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {
33 | "element_votes_element_id_element_atomicNumber_fk": {
34 | "name": "element_votes_element_id_element_atomicNumber_fk",
35 | "tableFrom": "element_votes",
36 | "tableTo": "element",
37 | "columnsFrom": [
38 | "element_id"
39 | ],
40 | "columnsTo": [
41 | "atomicNumber"
42 | ],
43 | "onDelete": "no action",
44 | "onUpdate": "no action"
45 | },
46 | "element_votes_user_id_users_id_fk": {
47 | "name": "element_votes_user_id_users_id_fk",
48 | "tableFrom": "element_votes",
49 | "tableTo": "users",
50 | "columnsFrom": [
51 | "user_id"
52 | ],
53 | "columnsTo": [
54 | "id"
55 | ],
56 | "onDelete": "no action",
57 | "onUpdate": "no action"
58 | }
59 | },
60 | "compositePrimaryKeys": {},
61 | "uniqueConstraints": {
62 | "element_votes_user_id_unique": {
63 | "name": "element_votes_user_id_unique",
64 | "nullsNotDistinct": false,
65 | "columns": [
66 | "user_id"
67 | ]
68 | }
69 | }
70 | },
71 | "element": {
72 | "name": "element",
73 | "schema": "",
74 | "columns": {
75 | "name": {
76 | "name": "name",
77 | "type": "text",
78 | "primaryKey": false,
79 | "notNull": true
80 | },
81 | "symbol": {
82 | "name": "symbol",
83 | "type": "varchar(3)",
84 | "primaryKey": false,
85 | "notNull": true
86 | },
87 | "atomicNumber": {
88 | "name": "atomicNumber",
89 | "type": "integer",
90 | "primaryKey": true,
91 | "notNull": true
92 | }
93 | },
94 | "indexes": {},
95 | "foreignKeys": {},
96 | "compositePrimaryKeys": {},
97 | "uniqueConstraints": {}
98 | },
99 | "users": {
100 | "name": "users",
101 | "schema": "",
102 | "columns": {
103 | "id": {
104 | "name": "id",
105 | "type": "text",
106 | "primaryKey": true,
107 | "notNull": true
108 | }
109 | },
110 | "indexes": {},
111 | "foreignKeys": {},
112 | "compositePrimaryKeys": {},
113 | "uniqueConstraints": {}
114 | }
115 | },
116 | "enums": {},
117 | "schemas": {},
118 | "_meta": {
119 | "columns": {},
120 | "schemas": {},
121 | "tables": {}
122 | }
123 | }
--------------------------------------------------------------------------------
/src/app/db/migrations/meta/0001_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "18094a25-10f8-4a72-817f-c9c2c6a2e51c",
3 | "prevId": "fc858a9a-ca71-4f1c-9956-806ad458fb6f",
4 | "version": "5",
5 | "dialect": "pg",
6 | "tables": {
7 | "element_votes": {
8 | "name": "element_votes",
9 | "schema": "",
10 | "columns": {
11 | "element_id": {
12 | "name": "element_id",
13 | "type": "integer",
14 | "primaryKey": false,
15 | "notNull": true
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "create_ts": {
24 | "name": "create_ts",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": true,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {
33 | "element_votes_element_id_element_atomicNumber_fk": {
34 | "name": "element_votes_element_id_element_atomicNumber_fk",
35 | "tableFrom": "element_votes",
36 | "tableTo": "element",
37 | "columnsFrom": [
38 | "element_id"
39 | ],
40 | "columnsTo": [
41 | "atomicNumber"
42 | ],
43 | "onDelete": "no action",
44 | "onUpdate": "no action"
45 | },
46 | "element_votes_user_id_users_id_fk": {
47 | "name": "element_votes_user_id_users_id_fk",
48 | "tableFrom": "element_votes",
49 | "tableTo": "users",
50 | "columnsFrom": [
51 | "user_id"
52 | ],
53 | "columnsTo": [
54 | "id"
55 | ],
56 | "onDelete": "no action",
57 | "onUpdate": "no action"
58 | }
59 | },
60 | "compositePrimaryKeys": {},
61 | "uniqueConstraints": {
62 | "element_votes_user_id_unique": {
63 | "name": "element_votes_user_id_unique",
64 | "nullsNotDistinct": false,
65 | "columns": [
66 | "user_id"
67 | ]
68 | }
69 | }
70 | },
71 | "element": {
72 | "name": "element",
73 | "schema": "",
74 | "columns": {
75 | "name": {
76 | "name": "name",
77 | "type": "text",
78 | "primaryKey": false,
79 | "notNull": true
80 | },
81 | "symbol": {
82 | "name": "symbol",
83 | "type": "varchar(3)",
84 | "primaryKey": false,
85 | "notNull": true
86 | },
87 | "atomicNumber": {
88 | "name": "atomicNumber",
89 | "type": "integer",
90 | "primaryKey": true,
91 | "notNull": true
92 | }
93 | },
94 | "indexes": {},
95 | "foreignKeys": {},
96 | "compositePrimaryKeys": {},
97 | "uniqueConstraints": {}
98 | },
99 | "users": {
100 | "name": "users",
101 | "schema": "",
102 | "columns": {
103 | "id": {
104 | "name": "id",
105 | "type": "text",
106 | "primaryKey": true,
107 | "notNull": true
108 | },
109 | "create_ts": {
110 | "name": "create_ts",
111 | "type": "timestamp",
112 | "primaryKey": false,
113 | "notNull": true,
114 | "default": "now()"
115 | },
116 | "clerk_create_ts": {
117 | "name": "clerk_create_ts",
118 | "type": "timestamp",
119 | "primaryKey": false,
120 | "notNull": true
121 | }
122 | },
123 | "indexes": {},
124 | "foreignKeys": {},
125 | "compositePrimaryKeys": {},
126 | "uniqueConstraints": {}
127 | }
128 | },
129 | "enums": {},
130 | "schemas": {},
131 | "_meta": {
132 | "columns": {},
133 | "schemas": {},
134 | "tables": {}
135 | }
136 | }
--------------------------------------------------------------------------------
/src/app/db/migrations/meta/0002_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "5171fdef-27d3-46e9-9c40-9fe774c9e21f",
3 | "prevId": "18094a25-10f8-4a72-817f-c9c2c6a2e51c",
4 | "version": "5",
5 | "dialect": "pg",
6 | "tables": {
7 | "element_votes": {
8 | "name": "element_votes",
9 | "schema": "",
10 | "columns": {
11 | "element_id": {
12 | "name": "element_id",
13 | "type": "integer",
14 | "primaryKey": false,
15 | "notNull": true
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "create_ts": {
24 | "name": "create_ts",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": true,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {
33 | "element_votes_element_id_element_atomicNumber_fk": {
34 | "name": "element_votes_element_id_element_atomicNumber_fk",
35 | "tableFrom": "element_votes",
36 | "tableTo": "element",
37 | "columnsFrom": [
38 | "element_id"
39 | ],
40 | "columnsTo": [
41 | "atomicNumber"
42 | ],
43 | "onDelete": "cascade",
44 | "onUpdate": "no action"
45 | },
46 | "element_votes_user_id_users_id_fk": {
47 | "name": "element_votes_user_id_users_id_fk",
48 | "tableFrom": "element_votes",
49 | "tableTo": "users",
50 | "columnsFrom": [
51 | "user_id"
52 | ],
53 | "columnsTo": [
54 | "id"
55 | ],
56 | "onDelete": "no action",
57 | "onUpdate": "no action"
58 | }
59 | },
60 | "compositePrimaryKeys": {},
61 | "uniqueConstraints": {
62 | "element_votes_user_id_unique": {
63 | "name": "element_votes_user_id_unique",
64 | "nullsNotDistinct": false,
65 | "columns": [
66 | "user_id"
67 | ]
68 | }
69 | }
70 | },
71 | "element": {
72 | "name": "element",
73 | "schema": "",
74 | "columns": {
75 | "name": {
76 | "name": "name",
77 | "type": "text",
78 | "primaryKey": false,
79 | "notNull": true
80 | },
81 | "symbol": {
82 | "name": "symbol",
83 | "type": "varchar(3)",
84 | "primaryKey": false,
85 | "notNull": true
86 | },
87 | "atomicNumber": {
88 | "name": "atomicNumber",
89 | "type": "integer",
90 | "primaryKey": true,
91 | "notNull": true
92 | }
93 | },
94 | "indexes": {},
95 | "foreignKeys": {},
96 | "compositePrimaryKeys": {},
97 | "uniqueConstraints": {}
98 | },
99 | "users": {
100 | "name": "users",
101 | "schema": "",
102 | "columns": {
103 | "id": {
104 | "name": "id",
105 | "type": "text",
106 | "primaryKey": true,
107 | "notNull": true
108 | },
109 | "clerk_create_ts": {
110 | "name": "clerk_create_ts",
111 | "type": "timestamp",
112 | "primaryKey": false,
113 | "notNull": true
114 | },
115 | "create_ts": {
116 | "name": "create_ts",
117 | "type": "timestamp",
118 | "primaryKey": false,
119 | "notNull": true,
120 | "default": "now()"
121 | }
122 | },
123 | "indexes": {},
124 | "foreignKeys": {},
125 | "compositePrimaryKeys": {},
126 | "uniqueConstraints": {}
127 | }
128 | },
129 | "enums": {},
130 | "schemas": {},
131 | "_meta": {
132 | "columns": {},
133 | "schemas": {},
134 | "tables": {}
135 | }
136 | }
--------------------------------------------------------------------------------
/src/app/db/migrations/meta/0003_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "b7ba8ee3-6a0b-438d-a08e-525ba6c2d770",
3 | "prevId": "5171fdef-27d3-46e9-9c40-9fe774c9e21f",
4 | "version": "5",
5 | "dialect": "pg",
6 | "tables": {
7 | "element_votes": {
8 | "name": "element_votes",
9 | "schema": "",
10 | "columns": {
11 | "element_id": {
12 | "name": "element_id",
13 | "type": "integer",
14 | "primaryKey": false,
15 | "notNull": true
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "create_ts": {
24 | "name": "create_ts",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": true,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {
33 | "element_votes_element_id_element_atomicNumber_fk": {
34 | "name": "element_votes_element_id_element_atomicNumber_fk",
35 | "tableFrom": "element_votes",
36 | "tableTo": "element",
37 | "columnsFrom": [
38 | "element_id"
39 | ],
40 | "columnsTo": [
41 | "atomicNumber"
42 | ],
43 | "onDelete": "no action",
44 | "onUpdate": "no action"
45 | },
46 | "element_votes_user_id_users_id_fk": {
47 | "name": "element_votes_user_id_users_id_fk",
48 | "tableFrom": "element_votes",
49 | "tableTo": "users",
50 | "columnsFrom": [
51 | "user_id"
52 | ],
53 | "columnsTo": [
54 | "id"
55 | ],
56 | "onDelete": "cascade",
57 | "onUpdate": "no action"
58 | }
59 | },
60 | "compositePrimaryKeys": {},
61 | "uniqueConstraints": {
62 | "element_votes_user_id_unique": {
63 | "name": "element_votes_user_id_unique",
64 | "nullsNotDistinct": false,
65 | "columns": [
66 | "user_id"
67 | ]
68 | }
69 | }
70 | },
71 | "element": {
72 | "name": "element",
73 | "schema": "",
74 | "columns": {
75 | "name": {
76 | "name": "name",
77 | "type": "text",
78 | "primaryKey": false,
79 | "notNull": true
80 | },
81 | "symbol": {
82 | "name": "symbol",
83 | "type": "varchar(3)",
84 | "primaryKey": false,
85 | "notNull": true
86 | },
87 | "atomicNumber": {
88 | "name": "atomicNumber",
89 | "type": "integer",
90 | "primaryKey": true,
91 | "notNull": true
92 | }
93 | },
94 | "indexes": {},
95 | "foreignKeys": {},
96 | "compositePrimaryKeys": {},
97 | "uniqueConstraints": {}
98 | },
99 | "users": {
100 | "name": "users",
101 | "schema": "",
102 | "columns": {
103 | "id": {
104 | "name": "id",
105 | "type": "text",
106 | "primaryKey": true,
107 | "notNull": true
108 | },
109 | "clerk_create_ts": {
110 | "name": "clerk_create_ts",
111 | "type": "timestamp",
112 | "primaryKey": false,
113 | "notNull": true
114 | },
115 | "create_ts": {
116 | "name": "create_ts",
117 | "type": "timestamp",
118 | "primaryKey": false,
119 | "notNull": true,
120 | "default": "now()"
121 | }
122 | },
123 | "indexes": {},
124 | "foreignKeys": {},
125 | "compositePrimaryKeys": {},
126 | "uniqueConstraints": {}
127 | }
128 | },
129 | "enums": {},
130 | "schemas": {},
131 | "_meta": {
132 | "columns": {},
133 | "schemas": {},
134 | "tables": {}
135 | }
136 | }
--------------------------------------------------------------------------------
/src/app/db/migrations/meta/0004_snapshot.json:
--------------------------------------------------------------------------------
1 | {
2 | "id": "fe42788a-7b2f-4a29-bc70-7935a76a920c",
3 | "prevId": "b7ba8ee3-6a0b-438d-a08e-525ba6c2d770",
4 | "version": "5",
5 | "dialect": "pg",
6 | "tables": {
7 | "element_votes": {
8 | "name": "element_votes",
9 | "schema": "",
10 | "columns": {
11 | "element_id": {
12 | "name": "element_id",
13 | "type": "integer",
14 | "primaryKey": false,
15 | "notNull": true
16 | },
17 | "user_id": {
18 | "name": "user_id",
19 | "type": "text",
20 | "primaryKey": false,
21 | "notNull": true
22 | },
23 | "create_ts": {
24 | "name": "create_ts",
25 | "type": "timestamp",
26 | "primaryKey": false,
27 | "notNull": true,
28 | "default": "now()"
29 | }
30 | },
31 | "indexes": {},
32 | "foreignKeys": {
33 | "element_votes_element_id_element_atomic_number_fk": {
34 | "name": "element_votes_element_id_element_atomic_number_fk",
35 | "tableFrom": "element_votes",
36 | "tableTo": "element",
37 | "columnsFrom": [
38 | "element_id"
39 | ],
40 | "columnsTo": [
41 | "atomic_number"
42 | ],
43 | "onDelete": "no action",
44 | "onUpdate": "no action"
45 | },
46 | "element_votes_user_id_users_id_fk": {
47 | "name": "element_votes_user_id_users_id_fk",
48 | "tableFrom": "element_votes",
49 | "tableTo": "users",
50 | "columnsFrom": [
51 | "user_id"
52 | ],
53 | "columnsTo": [
54 | "id"
55 | ],
56 | "onDelete": "cascade",
57 | "onUpdate": "no action"
58 | }
59 | },
60 | "compositePrimaryKeys": {},
61 | "uniqueConstraints": {
62 | "element_votes_user_id_unique": {
63 | "name": "element_votes_user_id_unique",
64 | "nullsNotDistinct": false,
65 | "columns": [
66 | "user_id"
67 | ]
68 | }
69 | }
70 | },
71 | "element": {
72 | "name": "element",
73 | "schema": "",
74 | "columns": {
75 | "name": {
76 | "name": "name",
77 | "type": "text",
78 | "primaryKey": false,
79 | "notNull": true
80 | },
81 | "symbol": {
82 | "name": "symbol",
83 | "type": "varchar(3)",
84 | "primaryKey": false,
85 | "notNull": true
86 | },
87 | "atomic_number": {
88 | "name": "atomic_number",
89 | "type": "integer",
90 | "primaryKey": true,
91 | "notNull": true
92 | }
93 | },
94 | "indexes": {},
95 | "foreignKeys": {},
96 | "compositePrimaryKeys": {},
97 | "uniqueConstraints": {}
98 | },
99 | "users": {
100 | "name": "users",
101 | "schema": "",
102 | "columns": {
103 | "id": {
104 | "name": "id",
105 | "type": "text",
106 | "primaryKey": true,
107 | "notNull": true
108 | },
109 | "clerk_create_ts": {
110 | "name": "clerk_create_ts",
111 | "type": "timestamp",
112 | "primaryKey": false,
113 | "notNull": true
114 | },
115 | "create_ts": {
116 | "name": "create_ts",
117 | "type": "timestamp",
118 | "primaryKey": false,
119 | "notNull": true,
120 | "default": "now()"
121 | }
122 | },
123 | "indexes": {},
124 | "foreignKeys": {},
125 | "compositePrimaryKeys": {},
126 | "uniqueConstraints": {}
127 | }
128 | },
129 | "enums": {},
130 | "schemas": {},
131 | "_meta": {
132 | "columns": {},
133 | "schemas": {},
134 | "tables": {}
135 | }
136 | }
--------------------------------------------------------------------------------
/src/app/db/migrations/meta/_journal.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "5",
3 | "dialect": "pg",
4 | "entries": [
5 | {
6 | "idx": 0,
7 | "version": "5",
8 | "when": 1710799135647,
9 | "tag": "0000_red_proemial_gods",
10 | "breakpoints": true
11 | },
12 | {
13 | "idx": 1,
14 | "version": "5",
15 | "when": 1710871241685,
16 | "tag": "0001_abandoned_rachel_grey",
17 | "breakpoints": true
18 | },
19 | {
20 | "idx": 2,
21 | "version": "5",
22 | "when": 1710874857877,
23 | "tag": "0002_nappy_obadiah_stane",
24 | "breakpoints": true
25 | },
26 | {
27 | "idx": 3,
28 | "version": "5",
29 | "when": 1710874997922,
30 | "tag": "0003_panoramic_marten_broadcloak",
31 | "breakpoints": true
32 | },
33 | {
34 | "idx": 4,
35 | "version": "5",
36 | "when": 1710883427375,
37 | "tag": "0004_square_falcon",
38 | "breakpoints": true
39 | }
40 | ]
41 | }
--------------------------------------------------------------------------------
/src/app/db/schema.ts:
--------------------------------------------------------------------------------
1 | import { integer, pgTable, text, timestamp, varchar } from 'drizzle-orm/pg-core';
2 |
3 | /**
4 | * This table stores users. Users are created in Clerk, then Clerk calls a
5 | * webhook at /api/webhook/clerk to inform this application a user was created.
6 | */
7 | export const Users = pgTable('users', {
8 | // The ID will be the user's ID from Clerk
9 | id: text('id').primaryKey().notNull(),
10 |
11 | // Time user was created in Clerk
12 | clerkCreateTs: timestamp('clerk_create_ts').notNull(),
13 |
14 | // Time when user was created in our database.
15 | createTs: timestamp('create_ts').defaultNow().notNull()
16 | });
17 |
18 | export const Elements = pgTable('element', {
19 | name: text('name').notNull(),
20 | symbol: varchar('symbol', { length: 3 }).notNull(),
21 | atomicNumber: integer('atomic_number').notNull().primaryKey(),
22 | });
23 |
24 | export const ElementVotes = pgTable('element_votes', {
25 | elementId: integer('element_id').references(() => Elements.atomicNumber).notNull(),
26 | userId: text('user_id').references(() => Users.id, { onDelete: 'cascade' }).unique().notNull(),
27 | createTs: timestamp('create_ts').defaultNow().notNull()
28 | });
29 |
30 | export type User = typeof Users.$inferSelect;
31 | export type Element = typeof Elements.$inferSelect;
32 | export type ElementVote = typeof ElementVotes.$inferSelect;
33 |
--------------------------------------------------------------------------------
/src/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/raoufchebri/neon-clerk-drizzle-nextjs/f3929bbe4197e32dbff9ff89ec9c520c198d4ac3/src/app/favicon.ico
--------------------------------------------------------------------------------
/src/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | @media (prefers-color-scheme: dark) {
12 | :root {
13 | --foreground-rgb: 255, 255, 255;
14 | --background-start-rgb: 0, 0, 0;
15 | --background-end-rgb: 0, 0, 0;
16 | }
17 | }
18 |
19 | body {
20 | color: rgb(var(--foreground-rgb));
21 | background: linear-gradient(
22 | to bottom,
23 | transparent,
24 | rgb(var(--background-end-rgb))
25 | )
26 | rgb(var(--background-start-rgb));
27 | }
28 |
29 | @layer utilities {
30 | .text-balance {
31 | text-wrap: balance;
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 | import { ClerkProvider, UserButton } from "@clerk/nextjs";
5 |
6 | const inter = Inter({ subsets: ["latin"] });
7 |
8 | export const metadata: Metadata = {
9 | title: "Create Next App",
10 | description: "Generated by create next app",
11 | };
12 |
13 | export default function RootLayout({
14 | children,
15 | }: Readonly<{
16 | children: React.ReactNode;
17 | }>) {
18 | return (
19 |
20 |
21 |
22 |
23 |
24 |
25 | {children}
26 |
27 |
28 |
29 | );
30 | }
31 |
--------------------------------------------------------------------------------
/src/app/log.ts:
--------------------------------------------------------------------------------
1 | import pino from 'pino'
2 |
3 | export const log = pino({
4 | level: process.env.LOG_LEVEL ? process.env.LOG_LEVEL : 'info'
5 | })
6 |
--------------------------------------------------------------------------------
/src/app/page.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "./db";
2 | import { ElementVotes, Elements } from "./db/schema";
3 | import { eq, sql } from 'drizzle-orm'
4 | import { currentUser } from '@clerk/nextjs';
5 | import { ElementComponent } from "./components/element";
6 | import Link from 'next/link'
7 |
8 | export const dynamic = 'force-dynamic';
9 |
10 | const getData = async () => {
11 | const user = await currentUser()
12 |
13 | if (!user) {
14 | throw new Error('user must be defined')
15 | }
16 |
17 | const vote = await db.select().from(ElementVotes).where(
18 | eq(ElementVotes.userId, user.id)
19 | )
20 |
21 | const elements = await db.
22 | select({
23 | votes: sql`COALESCE(COUNT(DISTINCT ${ElementVotes.userId}), 0)`,
24 | name: Elements.name,
25 | atomicNumber: Elements.atomicNumber,
26 | symbol: Elements.symbol
27 | })
28 | .from(Elements)
29 | .leftJoin(ElementVotes, eq(Elements.atomicNumber, ElementVotes.elementId))
30 | .groupBy(sql`${Elements.name},${Elements.atomicNumber}`)
31 |
32 |
33 | return { elements, vote };
34 | };
35 |
36 | export default async function Home() {
37 | const { elements, vote } = await getData();
38 | let message = 'Vote for your favourite element by clicking on it.'
39 |
40 | if (vote[0]) {
41 | message = `You voted for ${elements.find(el => el.atomicNumber === vote[0].elementId)?.name}! Change your vote by clicking an element.`
42 | elements.sort((e1, e2) => {
43 | return e1.votes > e2.votes ? -1 : 1
44 | })
45 | }
46 |
47 | return (
48 |
49 |
52 |
53 |
54 | {
55 | elements.map(el => {
56 | return (
57 |
58 |
59 | Votes: {el.votes}
60 |
61 |
62 | )
63 | })
64 | }
65 |
66 |
67 |
68 | );
69 | }
70 |
--------------------------------------------------------------------------------
/src/app/voted/page.tsx:
--------------------------------------------------------------------------------
1 | import { db } from "@/app/db";
2 | import { ElementVotes } from "@/app/db/schema";
3 | import { eq } from 'drizzle-orm'
4 | import { currentUser } from '@clerk/nextjs';
5 | import { redirect } from 'next/navigation'
6 | import { log } from "../log";
7 |
8 | export const dynamic = 'force-dynamic';
9 |
10 | const voteAndGetData = async (elementId?: string) => {
11 | if (!elementId) {
12 | log.warn('a user visited /voted without specifying an elementId')
13 | return redirect('/')
14 | }
15 |
16 | const user = await currentUser()
17 |
18 | if (!user) {
19 | throw new Error('user must be defined')
20 | }
21 |
22 | const existingVote = await db
23 | .select()
24 | .from(ElementVotes)
25 | .where(
26 | eq(ElementVotes.userId, user.id)
27 | )
28 |
29 | if (existingVote[0]) {
30 | // User has already voted, update it
31 | log.info(`updating user ${user.id} vote`)
32 | return db
33 | .update(ElementVotes)
34 | .set({
35 | elementId: parseInt(elementId)
36 | })
37 | .where(eq(ElementVotes.userId, user.id))
38 | .returning()
39 | } else {
40 | log.info(`insert vote for user ${user.id}`)
41 | return db
42 | .insert(ElementVotes)
43 | .values({
44 | userId: user.id,
45 | elementId: parseInt(elementId)
46 | }).returning()
47 | }
48 | };
49 |
50 | export default async function Voted({
51 | searchParams,
52 | }: {
53 | searchParams: { [key: string]: string | string[] | undefined };
54 | }) {
55 | const { elementId } = searchParams
56 |
57 | await voteAndGetData(elementId as string)
58 |
59 | return redirect(`/?element=${elementId}`)
60 | }
61 |
--------------------------------------------------------------------------------
/src/middleware.ts:
--------------------------------------------------------------------------------
1 | import { authMiddleware } from "@clerk/nextjs";
2 |
3 | export default authMiddleware({
4 | // Routes that can be accessed while signed out
5 | publicRoutes: ['/api/webhooks/clerk']
6 | });
7 |
8 | export const config = {
9 | // Protects all routes, including api/trpc.
10 | // See https://clerk.com/docs/references/nextjs/auth-middleware
11 | // for more information about configuring your Middleware
12 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"],
13 | };
14 |
--------------------------------------------------------------------------------
/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./src/pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./src/components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./src/app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./src/*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------