diff --git a/bun.lockb b/bun.lockb index c1357ac..cb76206 100755 Binary files a/bun.lockb and b/bun.lockb differ diff --git a/drizzle/0000_wandering_steve_rogers.sql b/drizzle/0000_burly_katie_power.sql similarity index 88% rename from drizzle/0000_wandering_steve_rogers.sql rename to drizzle/0000_burly_katie_power.sql index d90a2f4..1e7c7fb 100644 --- a/drizzle/0000_wandering_steve_rogers.sql +++ b/drizzle/0000_burly_katie_power.sql @@ -15,13 +15,15 @@ CREATE TABLE IF NOT EXISTS "accounts" ( --> statement-breakpoint CREATE TABLE IF NOT EXISTS "images" ( "id" varchar(255) PRIMARY KEY NOT NULL, + "slug" varchar(255) NOT NULL, "title" varchar(256), "description" varchar, "tags" text[], "filepath" varchar(256), "created_by" varchar(255) NOT NULL, "created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, - "updated_at" timestamp with time zone + "updated_at" timestamp with time zone, + CONSTRAINT "images_slug_unique" UNIQUE("slug") ); --> statement-breakpoint CREATE TABLE IF NOT EXISTS "sessions" ( @@ -65,4 +67,6 @@ EXCEPTION END $$; --> statement-breakpoint CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "accounts" USING btree ("user_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "image_user_id_idx" ON "images" USING btree ("created_by");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "image_slug_idx" ON "images" USING btree ("slug");--> statement-breakpoint CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "sessions" USING btree ("user_id"); \ No newline at end of file diff --git a/drizzle/meta/0000_snapshot.json b/drizzle/meta/0000_snapshot.json index 0be79c4..390a6e7 100644 --- a/drizzle/meta/0000_snapshot.json +++ b/drizzle/meta/0000_snapshot.json @@ -1,5 +1,5 @@ { - "id": "f7541b9a-140c-4795-a672-4d0c80b56ef1", + "id": "6010a064-33aa-404b-b933-c4ca1f1c415d", "prevId": "00000000-0000-0000-0000-000000000000", "version": "7", "dialect": "postgresql", @@ -128,6 +128,12 @@ "primaryKey": true, "notNull": true }, + "slug": { + "name": "slug", + "type": "varchar(255)", + "primaryKey": false, + "notNull": true + }, "title": { "name": "title", "type": "varchar(256)", @@ -172,7 +178,38 @@ "notNull": false } }, - "indexes": {}, + "indexes": { + "image_user_id_idx": { + "name": "image_user_id_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "image_slug_idx": { + "name": "image_slug_idx", + "columns": [ + { + "expression": "slug", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, "foreignKeys": { "images_created_by_users_id_fk": { "name": "images_created_by_users_id_fk", @@ -189,7 +226,15 @@ } }, "compositePrimaryKeys": {}, - "uniqueConstraints": {} + "uniqueConstraints": { + "images_slug_unique": { + "name": "images_slug_unique", + "nullsNotDistinct": false, + "columns": [ + "slug" + ] + } + } }, "public.sessions": { "name": "sessions", diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 80007ba..b5e9169 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -5,8 +5,8 @@ { "idx": 0, "version": "7", - "when": 1726662474693, - "tag": "0000_wandering_steve_rogers", + "when": 1726843311417, + "tag": "0000_burly_katie_power", "breakpoints": true } ] diff --git a/next.config.js b/next.config.js index 74f3878..c2528bd 100644 --- a/next.config.js +++ b/next.config.js @@ -14,6 +14,9 @@ const config = { { hostname: "opengifame.dev.fergl.ie", }, + { + hostname: "cloudflare-ipfs.com", + }, { hostname: "localhost", }, diff --git a/package.json b/package.json index 5ce4bd5..64be64a 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-tooltip": "^1.1.2", + "@sindresorhus/slugify": "^2.2.1", "@t3-oss/env-nextjs": "^0.11.1", "@tanstack/react-query": "^5.56.2", "@trpc/client": "^11.0.0-rc.446", diff --git a/scripts/scaffold.sql b/scripts/scaffold.sql new file mode 100644 index 0000000..c9f96f8 --- /dev/null +++ b/scripts/scaffold.sql @@ -0,0 +1,12 @@ +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('974248ec-ae31-4c1a-900d-348432e16e90', 'Frasier "directs" Niles', 'Ham Radio', '{}', '974248ec-ae31-4c1a-900d-348432e16e90.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:26:51.350362 +00:00', '2024-09-18 15:26:51.639000 +00:00', 'frasier-directs-niles'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('18fac61f-d2c4-4248-b74b-5d23e8717bbc', 'Frasier groaning.', 'Frasier & Niles in Nervosa', '{}', '18fac61f-d2c4-4248-b74b-5d23e8717bbc.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:46.438441 +00:00', '2024-09-18 15:27:46.477000 +00:00', 'frasier-groaning'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('56e89352-aa69-4a9e-92c4-5eb3e8e94703', 'Frasier tasting caviar', 'From Roe to Perdition S10E18 ', '{}', '56e89352-aa69-4a9e-92c4-5eb3e8e94703.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:28.599836 +00:00', '2024-09-18 15:27:28.632000 +00:00', 'frasier-tasting-caviar'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('38d3e400-20a3-49be-bf8f-5c4c77ab3a9f', 'Niles gets a class of champagne in the face', 'From Voyage of the Damned', '{}', '38d3e400-20a3-49be-bf8f-5c4c77ab3a9f.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:01.933852 +00:00', '2024-09-18 15:27:01.967000 +00:00', 'niles-gets-a-class-of-champagne-in-the-face'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('4aa77fab-cfe4-4f5d-ba62-c674b43b09c8', 'I miss being unapproachable', 'From Good Grief', '{}', '4aa77fab-cfe4-4f5d-ba62-c674b43b09c8.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:18.617006 +00:00', '2024-09-18 15:27:18.651000 +00:00', 'i-miss-being-unapproachable'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('062edc3f-2d3c-4d97-a569-f41804d6d352', 'I''m having one now', 'Niles has never had an unexpressed thought', '{niles,frasier}', '062edc3f-2d3c-4d97-a569-f41804d6d352.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:59.258519 +00:00', '2024-09-18 15:27:59.283000 +00:00', 'i-m-having-one-now'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('ced6dc13-10b4-43d7-8072-0db2091840d7', 'Wounded!!!', 'Frasier is wounded', '{frasier,wounded}', 'ced6dc13-10b4-43d7-8072-0db2091840d7.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:37.875584 +00:00', '2024-09-18 15:27:37.914000 +00:00', 'wounded'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('13c54e08-2fd6-4810-982f-14b701e38b5f', 'Rrrrreally.', 'Guy checks out Niles'' bottom', '{}', '13c54e08-2fd6-4810-982f-14b701e38b5f.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:13.414832 +00:00', '2024-09-18 15:27:13.442000 +00:00', 'rrrrreally'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('d23df48f-2b99-4b29-90fd-2f3cff205010', 'Successful High Five', 'Niles & Frasier successfully complete a high five (including behind the back down low).', '{}', 'd23df48f-2b99-4b29-90fd-2f3cff205010.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:51.131650 +00:00', '2024-09-18 15:27:51.152000 +00:00', 'successful-high-five'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('0ccfccd4-c24f-42a0-9864-2cb8d230bb16', 'Niles clapping', 'Niles is excited', '{}', '0ccfccd4-c24f-42a0-9864-2cb8d230bb16.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:33.156638 +00:00', '2024-09-18 15:27:33.220000 +00:00', 'niles-clapping'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('4a208725-453d-4d0c-b620-cc82b7192ea8', 'I just want to die!!', '', '{}', '4a208725-453d-4d0c-b620-cc82b7192ea8.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:07.374464 +00:00', '2024-09-18 15:27:07.396000 +00:00', 'i-just-want-to-die'); +INSERT INTO public.images (id, title, description, tags, filepath, created_by, created_at, updated_at, slug) VALUES ('4a2a52a2-85eb-417c-bdb8-a1d0101df17f', 'Some boys go to college.', 'Oh, some boys go to college, but we think they''re all wussies, cuz they get all the knowledge, and we get all the...', '{}', '4a2a52a2-85eb-417c-bdb8-a1d0101df17f.gif', '6c98e4e0-873c-4bd0-a059-93177709eddd', '2024-09-18 15:27:42.138300 +00:00', '2024-09-18 15:27:42.175000 +00:00', 'some-boys-go-to-college'); diff --git a/src/app/(site)/(app)/image/[slug]/page.tsx b/src/app/(site)/(app)/image/[slug]/page.tsx new file mode 100644 index 0000000..60316a9 --- /dev/null +++ b/src/app/(site)/(app)/image/[slug]/page.tsx @@ -0,0 +1,13 @@ +import { api } from "@/trpc/server"; + +type ImageRouteParams = { + slug: string; +}; +const ImageRoute = async ({ params }: { params: ImageRouteParams }) => { + // const ImageRoute = ({ slug }: ImageRouteParams) => { + + const image = await api.image.getBySlug(); + return
Me so imagey {params.slug}
; +}; + +export default ImageRoute; diff --git a/src/app/(site)/upload/page.tsx b/src/app/(site)/(app)/upload/page.tsx similarity index 100% rename from src/app/(site)/upload/page.tsx rename to src/app/(site)/(app)/upload/page.tsx diff --git a/src/components/pages/image/image-page.tsx b/src/components/pages/image/image-page.tsx new file mode 100644 index 0000000..66d1cfd --- /dev/null +++ b/src/components/pages/image/image-page.tsx @@ -0,0 +1,12 @@ +import React from "react"; +import { Post } from "@/lib/models/post"; + +type ImagePageProps = { + post: Post; +}; + +const ImagePage: React.FC = ({ post }) => { + return
{post.title}
; +}; + +export default ImagePage; diff --git a/src/components/widgets/user-nav-dropdown.tsx b/src/components/widgets/user-nav-dropdown.tsx index d36a456..8e8801d 100644 --- a/src/components/widgets/user-nav-dropdown.tsx +++ b/src/components/widgets/user-nav-dropdown.tsx @@ -15,7 +15,6 @@ const UserNavDropdown: React.FC = ({ session }) => { session?.user?.image || "/images/default-profile.jpg", ); React.useEffect(() => { - logger.debug("UserNavDropdown", "session", session); setProfileImage(session?.user?.image || "/images/default-profile.jpg"); }, [session]); diff --git a/src/lib/models/post.ts b/src/lib/models/post.ts new file mode 100644 index 0000000..0593fad --- /dev/null +++ b/src/lib/models/post.ts @@ -0,0 +1,8 @@ +export type Post = { + slug: string; + title: string; + description: string; + likes: number; + dislikes: number; + datePosted: Date; +} \ No newline at end of file diff --git a/src/server/api/routers/image.ts b/src/server/api/routers/image.ts index 41af296..e386aea 100644 --- a/src/server/api/routers/image.ts +++ b/src/server/api/routers/image.ts @@ -3,17 +3,34 @@ import { protectedProcedure, publicProcedure, } from "@/server/api/trpc"; +import { slugifyWithCounter } from "@sindresorhus/slugify"; import { z } from "zod"; -import { images } from "@/server/db/schema"; +import { images, users } from "@/server/db/schema"; import { env } from "@/env"; - -const imageCreateType = { - title: z.string().min(5), - description: z.string(), - tags: z.array(z.string()), -}; +import { and, eq } from "drizzle-orm"; export const imageRouter = createTRPCRouter({ + getBySlug: publicProcedure + .input(z.object({ slug: z.string() })) + .query(async ({ ctx, input }) => { + const image = ( + await ctx.db + .select() + .from(images) + .where(and(eq(images.slug, input.slug))) + .limit(1) + )[0]; + if (!image) { + throw new Error("Image not found"); + } + return { + id: image.id, + title: image.title, + description: image.description, + tags: image.tags, + url: `${env.IMAGE_BASE_URL}/${image.filePath}`, + }; + }), getTrending: publicProcedure.query(async ({ ctx }) => { const trending = await ctx.db.query.images.findMany({ orderBy: (images, { desc }) => [desc(images.createdAt)], @@ -31,12 +48,34 @@ export const imageRouter = createTRPCRouter({ ); }), create: protectedProcedure - .input(z.object(imageCreateType)) + .input( + z.object({ + title: z.string().min(5), + description: z.string(), + tags: z.array(z.string()), + }), + ) .mutation(async ({ ctx, input }) => { + var slugify = slugifyWithCounter(); + var found = false; + var slug = ""; + do { + slug = slugify(input.title); + const existing = await ctx.db + .select() + .from(images) + .where(and(eq(images.slug, slug))) + .limit(1); + if (!existing[0]) { + found = true; + } + } while (!found); + const post = await ctx.db .insert(images) .values({ title: input.title, + slug: slug, description: input.description, tags: input.tags, createdById: ctx.session.user.id, diff --git a/src/server/db/schema.ts b/src/server/db/schema.ts index e3665c0..946eada 100644 --- a/src/server/db/schema.ts +++ b/src/server/db/schema.ts @@ -13,25 +13,35 @@ import { type AdapterAccount } from "next-auth/adapters"; export const createTable = pgTableCreator((name) => `${name}`); -export const images = createTable("images", { - id: varchar("id", { length: 255 }) - .notNull() - .primaryKey() - .$defaultFn(() => crypto.randomUUID()), - title: varchar("title", { length: 256 }), - description: varchar("description"), - tags: text("tags").array(), - filePath: varchar("filepath", { length: 256 }), - createdById: varchar("created_by", { length: 255 }) - .notNull() - .references(() => users.id), - createdAt: timestamp("created_at", { withTimezone: true }) - .default(sql`CURRENT_TIMESTAMP`) - .notNull(), - updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( - () => new Date(), - ), -}); +export const images = createTable( + "images", + { + id: varchar("id", { length: 255 }) + .notNull() + .primaryKey() + .$defaultFn(() => crypto.randomUUID()), + slug: varchar("slug", { length: 255 }).notNull().unique(), + title: varchar("title", { length: 256 }), + description: varchar("description"), + tags: text("tags").array(), + filePath: varchar("filepath", { length: 256 }), + createdById: varchar("created_by", { length: 255 }) + .notNull() + .references(() => users.id), + createdAt: timestamp("created_at", { withTimezone: true }) + .default(sql`CURRENT_TIMESTAMP`) + .notNull(), + updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( + () => new Date(), + ), + }, + (table) => { + return { + userIndex: index("image_user_id_idx").on(table.createdById), + slugIndex: index("image_slug_idx").on(table.slug), + }; + }, +); export const users = createTable("users", { id: varchar("id", { length: 255 })