From 331a6f32cab44136a2a96f09b98a8dd22b87748b Mon Sep 17 00:00:00 2001 From: Web Dev Cody Date: Fri, 2 Feb 2024 08:21:47 -0500 Subject: [PATCH] adding tabs for gallery and grid views --- convex/_generated/api.d.ts | 2 + convex/thumbnails.ts | 74 +++++++- convex/users.ts | 13 ++ convex/util.ts | 24 +++ convex/vision.ts | 49 +++++ package-lock.json | 211 +++++++++++++++++++++- package.json | 2 + src/app/create/page.tsx | 7 +- src/app/thumbnails/[thumbnailId]/page.tsx | 124 ++++++++----- src/components/ui/tabs.tsx | 55 ++++++ 10 files changed, 507 insertions(+), 54 deletions(-) create mode 100644 convex/vision.ts create mode 100644 src/components/ui/tabs.tsx diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index 9b86a39..c98e6f7 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -22,6 +22,7 @@ import type * as stripe from "../stripe.js"; import type * as thumbnails from "../thumbnails.js"; import type * as users from "../users.js"; import type * as util from "../util.js"; +import type * as vision from "../vision.js"; /** * A utility for referencing Convex functions in your app's API. @@ -40,6 +41,7 @@ declare const fullApi: ApiFromModules<{ thumbnails: typeof thumbnails; users: typeof users; util: typeof util; + vision: typeof vision; }>; export declare const api: FilterApi< typeof fullApi, diff --git a/convex/thumbnails.ts b/convex/thumbnails.ts index 1c8562d..008ccb1 100644 --- a/convex/thumbnails.ts +++ b/convex/thumbnails.ts @@ -1,23 +1,59 @@ import { ConvexError, v } from "convex/values"; -import { query } from "./_generated/server"; +import { action, internalMutation, query } from "./_generated/server"; import { paginationOptsValidator } from "convex/server"; -import { adminAuthMutation, authMutation, authQuery } from "./util"; +import { adminAuthMutation, authAction, authMutation, authQuery } from "./util"; +import { internal } from "./_generated/api"; +import { Id } from "./_generated/dataModel"; -export const createThumbnail = authMutation({ +export const createThumbnail = internalMutation({ args: { title: v.string(), images: v.array(v.string()), + userId: v.id("users"), }, handler: async (ctx, args) => { - return await ctx.db.insert("thumbnails", { + const user = await ctx.db.get(args.userId); + + if (!user) { + throw new ConvexError("User not found"); + } + + const id = await ctx.db.insert("thumbnails", { title: args.title, - userId: ctx.user._id, + userId: user._id, images: args.images, votes: args.images.map(() => 0), voteIds: [], - profileImage: ctx.user.profileImage, - name: ctx.user.name, + profileImage: user.profileImage, + name: user.name, }); + + return id; + }, +}); + +export const createThumbnailAction = authAction({ + args: { + title: v.string(), + images: v.array(v.id("_storage")), + }, + handler: async (ctx, args) => { + const thumbnailId: Id<"thumbnails"> = await ctx.runMutation( + internal.thumbnails.createThumbnail, + { + images: args.images, + title: args.title, + userId: ctx.user._id, + } + ); + + await ctx.scheduler.runAfter(0, internal.vision.generateAIComment, { + imageIds: args.images, + thumbnailId: thumbnailId, + userId: ctx.user._id, + }); + + return thumbnailId; }, }); @@ -53,6 +89,30 @@ export const addComment = authMutation({ }, }); +export const addCommentInternal = internalMutation({ + args: { + thumbnailId: v.id("thumbnails"), + text: v.string(), + userId: v.id("users"), + }, + handler: async (ctx, args) => { + const thumbnail = await ctx.db.get(args.thumbnailId); + + if (!thumbnail) { + throw new Error("thumbnail by id did not exist"); + } + + await ctx.db.insert("comments", { + createdAt: Date.now(), + text: args.text, + userId: args.userId, + thumbnailId: args.thumbnailId, + name: "GPT4 Vision", + profileUrl: "", + }); + }, +}); + export const getThumbnail = query({ args: { thumbnailId: v.id("thumbnails") }, handler: async (ctx, args) => { diff --git a/convex/users.ts b/convex/users.ts index 976a4c9..a301c72 100644 --- a/convex/users.ts +++ b/convex/users.ts @@ -3,6 +3,7 @@ import { MutationCtx, QueryCtx, internalMutation, + internalQuery, query, } from "./_generated/server"; import { authMutation, authQuery } from "./util"; @@ -16,6 +17,18 @@ export const getUser = authQuery({ }, }); +export const getUserById = internalQuery({ + args: { userId: v.string() }, + handler: async (ctx, args) => { + const user = await ctx.db + .query("users") + .withIndex("by_userId", (q) => q.eq("userId", args.userId)) + .first(); + + return user; + }, +}); + export const getProfile = query({ args: { userId: v.id("users") }, handler: async (ctx, args) => { diff --git a/convex/util.ts b/convex/util.ts index e867a50..e537025 100644 --- a/convex/util.ts +++ b/convex/util.ts @@ -2,15 +2,18 @@ import { ActionCtx, MutationCtx, QueryCtx, + action, mutation, } from "./_generated/server"; import { customQuery, customCtx, customMutation, + customAction, } from "convex-helpers/server/customFunctions"; import { query } from "./_generated/server"; import { ConvexError } from "convex/values"; +import { internal } from "./_generated/api"; export const authQuery = customQuery( query, @@ -23,6 +26,27 @@ export const authQuery = customQuery( }) ); +export const authAction = customAction( + action, + customCtx(async (ctx) => { + const userId = (await ctx.auth.getUserIdentity())?.subject; + + if (!userId) { + throw new ConvexError("must be logged in"); + } + + const user: any = await ctx.runQuery(internal.users.getUserById, { + userId, + }); + + if (!user) { + throw new ConvexError("user not found"); + } + + return { user }; + }) +); + export const authMutation = customMutation( mutation, customCtx(async (ctx) => ({ user: await getUserOrThrow(ctx) })) diff --git a/convex/vision.ts b/convex/vision.ts new file mode 100644 index 0000000..417c1de --- /dev/null +++ b/convex/vision.ts @@ -0,0 +1,49 @@ +import OpenAI from "openai"; +import { internalAction } from "./_generated/server"; +import { v } from "convex/values"; +import { api, internal } from "./_generated/api"; + +const openai = new OpenAI(); + +export const generateAIComment = internalAction({ + args: { + imageIds: v.array(v.id("_storage")), + thumbnailId: v.id("thumbnails"), + userId: v.id("users"), + }, + async handler(ctx, args) { + const images = await Promise.all( + args.imageIds.map(async (imageId) => ({ + type: "image_url", + image_url: { + url: await ctx.storage.getUrl(imageId), + }, + })) + ); + + const response = await openai.chat.completions.create({ + model: "gpt-4-vision-preview", + max_tokens: 1000, + messages: [ + { + role: "user", + content: [ + { + type: "text", + text: "Pick which Thumbnails Looks the Best:", + }, + ...(images as any), + ], + }, + ], + }); + + const content = response.choices[0].message.content; + + await ctx.runMutation(internal.thumbnails.addCommentInternal, { + thumbnailId: args.thumbnailId, + text: content ?? "", + userId: args.userId, + }); + }, +}); diff --git a/package-lock.json b/package-lock.json index 9db3750..107647e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@xixixao/uploadstuff": "^0.0.5", "class-variance-authority": "^0.7.0", @@ -29,6 +30,7 @@ "next-themes": "^0.2.1", "nextjs-toploader": "^1.6.4", "npm-run-all": "^4.1.5", + "openai": "^4.26.0", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.49.3", @@ -4905,6 +4907,36 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.0.4.tgz", + "integrity": "sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-direction": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-presence": "1.0.1", + "@radix-ui/react-primitive": "1.0.3", + "@radix-ui/react-roving-focus": "1.0.4", + "@radix-ui/react-use-controllable-state": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-toast": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.1.5.tgz", @@ -6178,6 +6210,17 @@ "url": "https://github.com/sponsors/dcastil" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -6221,6 +6264,17 @@ "node": ">=6.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -7297,6 +7351,11 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base-64": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/base-64/-/base-64-0.1.0.tgz", + "integrity": "sha512-Y5gU45svrR5tI2Vt/X9GPd3L0HNIKzGu202EjxrXMpuc2V2CiKgemAbUUsqYmZJvPtCXoUKjNZwBJzsNScUbXA==" + }, "node_modules/base64-js": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", @@ -8583,6 +8642,14 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/charenc": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.2.tgz", + "integrity": "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA==", + "engines": { + "node": "*" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -9166,6 +9233,14 @@ "node": ">= 8" } }, + "node_modules/crypt": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.2.tgz", + "integrity": "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==", + "engines": { + "node": "*" + } + }, "node_modules/crypto-js": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.2.0.tgz", @@ -9358,6 +9433,15 @@ "node": ">=0.3.1" } }, + "node_modules/digest-fetch": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/digest-fetch/-/digest-fetch-1.3.0.tgz", + "integrity": "sha512-CGJuv6iKNM7QyZlM2T3sPAdZWd/p9zQiRNS9G+9COUCwzWFTs0Xp8NF5iePx7wtvhDykReiRRrSeNb4oMmB8lA==", + "dependencies": { + "base-64": "^0.1.0", + "md5": "^2.3.0" + } + }, "node_modules/dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -10122,6 +10206,14 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/events": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", @@ -10493,6 +10585,31 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/forwarded": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", @@ -10950,6 +11067,14 @@ "node": ">= 0.8" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -11251,6 +11376,11 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" + }, "node_modules/is-callable": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", @@ -12050,6 +12180,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/md5": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/md5/-/md5-2.3.0.tgz", + "integrity": "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g==", + "dependencies": { + "charenc": "0.0.2", + "crypt": "0.0.2", + "is-buffer": "~1.1.6" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -12279,8 +12419,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mute-stream": { "version": "0.0.8", @@ -12442,6 +12581,24 @@ "tslib": "^2.0.3" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, "node_modules/node-fetch": { "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", @@ -12854,6 +13011,55 @@ "node": ">=6" } }, + "node_modules/openai": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.26.0.tgz", + "integrity": "sha512-HPC7tgYdeP38F3uHA5WgnoXZyGbAp9jgcIo23p6It+q/07u4C+NZ8xHKlMShsPbDDmFRpPsa3vdbXYpbhJH3eg==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "digest-fetch": "^1.3.0", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + }, + "bin": { + "openai": "bin/cli" + } + }, + "node_modules/openai/node_modules/@types/node": { + "version": "18.19.14", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.14.tgz", + "integrity": "sha512-EnQ4Us2rmOS64nHDWr0XqAD8DsO6f3XR6lf9UIIrZQpUzPVdN/oPuEzfDWNHSyXLvoGgjuEm/sPwFGSSs35Wtg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/openai/node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, + "node_modules/openai/node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/openid-client": { "version": "5.6.4", "resolved": "https://registry.npmjs.org/openid-client/-/openid-client-5.6.4.tgz", @@ -16068,7 +16274,6 @@ "version": "3.3.2", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.2.tgz", "integrity": "sha512-3pRGuxRF5gpuZc0W+EpwQRmCD7gRqcDOMt688KmdlDAgAyaB1XlN0zq2njfDNm44XVdIouE7pZ6GzbdyH47uIQ==", - "dev": true, "engines": { "node": ">= 8" } diff --git a/package.json b/package.json index 01679bb..d434426 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-progress": "^1.0.3", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-tabs": "^1.0.4", "@radix-ui/react-toast": "^1.1.5", "@xixixao/uploadstuff": "^0.0.5", "class-variance-authority": "^0.7.0", @@ -35,6 +36,7 @@ "next-themes": "^0.2.1", "nextjs-toploader": "^1.6.4", "npm-run-all": "^4.1.5", + "openai": "^4.26.0", "react": "^18", "react-dom": "^18", "react-hook-form": "^7.49.3", diff --git a/src/app/create/page.tsx b/src/app/create/page.tsx index 8eefc15..a583064 100644 --- a/src/app/create/page.tsx +++ b/src/app/create/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useMutation } from "convex/react"; +import { useAction, useMutation } from "convex/react"; import { api } from "../../../convex/_generated/api"; import { UploadButton, UploadFileResponse } from "@xixixao/uploadstuff/react"; import "@xixixao/uploadstuff/react/styles.css"; @@ -14,6 +14,7 @@ import clsx from "clsx"; import { useRouter } from "next/navigation"; import { getImageUrl } from "@/lib/utils"; import { UpgradeButton } from "@/components/upgrade-button"; +import { Id } from "../../../convex/_generated/dataModel"; const defaultErrorState = { title: "", @@ -22,11 +23,11 @@ const defaultErrorState = { export default function CreatePage() { const generateUploadUrl = useMutation(api.files.generateUploadUrl); - const createThumbnail = useMutation(api.thumbnails.createThumbnail); + const createThumbnail = useAction(api.thumbnails.createThumbnailAction); const { toast } = useToast(); const router = useRouter(); const [errors, setErrors] = useState(defaultErrorState); - const [images, setImages] = useState([]); + const [images, setImages] = useState[]>([]); return (
diff --git a/src/app/thumbnails/[thumbnailId]/page.tsx b/src/app/thumbnails/[thumbnailId]/page.tsx index 4fddca7..7eaa792 100644 --- a/src/app/thumbnails/[thumbnailId]/page.tsx +++ b/src/app/thumbnails/[thumbnailId]/page.tsx @@ -1,6 +1,6 @@ "use client"; -import { useConvexAuth, useMutation, useQuery } from "convex/react"; +import { useMutation, useQuery } from "convex/react"; import { useParams } from "next/navigation"; import { api } from "../../../../convex/_generated/api"; import { Doc, Id } from "../../../../convex/_generated/dataModel"; @@ -17,9 +17,12 @@ import { ArrowRightIcon, CheckCircleIcon, DotIcon, + GalleryHorizontal, + LayoutGrid, } from "lucide-react"; import Link from "next/link"; -import { useEffect, useState } from "react"; +import { useState } from "react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; function getVotesFor(thumbnail: Doc<"thumbnails">, imageId: string) { if (!thumbnail) return 0; @@ -161,53 +164,92 @@ export default function ThumbnailPage() {
{!hasVoted && ( -
- + <> + + + + Grid + + + Gallery + + -
- + +
+ {thumbnail.images.map((imageId) => { + return ( + + ); + })} +
+
-
- {currentImageIndex + 1} of {thumbnail.images.length} -
+ +
+ - -
-
+
+ + +
+ {currentImageIndex + 1} of {thumbnail.images.length} +
+ + +
+
+ + + )} {hasVoted && (
{sortedImages.map((imageId) => ( -
+
, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsList.displayName = TabsPrimitive.List.displayName; + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName; + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)); +TabsContent.displayName = TabsPrimitive.Content.displayName; + +export { Tabs, TabsList, TabsTrigger, TabsContent };