Slugifying URLs..

This commit is contained in:
Fergal Moran
2024-09-20 16:27:36 +01:00
parent 790648f355
commit bc80fc0d60
14 changed files with 180 additions and 34 deletions

BIN
bun.lockb

Binary file not shown.

View File

@@ -15,13 +15,15 @@ CREATE TABLE IF NOT EXISTS "accounts" (
--> statement-breakpoint --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "images" ( CREATE TABLE IF NOT EXISTS "images" (
"id" varchar(255) PRIMARY KEY NOT NULL, "id" varchar(255) PRIMARY KEY NOT NULL,
"slug" varchar(255) NOT NULL,
"title" varchar(256), "title" varchar(256),
"description" varchar, "description" varchar,
"tags" text[], "tags" text[],
"filepath" varchar(256), "filepath" varchar(256),
"created_by" varchar(255) NOT NULL, "created_by" varchar(255) NOT NULL,
"created_at" timestamp with time zone DEFAULT CURRENT_TIMESTAMP 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 --> statement-breakpoint
CREATE TABLE IF NOT EXISTS "sessions" ( CREATE TABLE IF NOT EXISTS "sessions" (
@@ -65,4 +67,6 @@ EXCEPTION
END $$; END $$;
--> statement-breakpoint --> statement-breakpoint
CREATE INDEX IF NOT EXISTS "account_user_id_idx" ON "accounts" USING btree ("user_id");--> 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"); CREATE INDEX IF NOT EXISTS "session_user_id_idx" ON "sessions" USING btree ("user_id");

View File

@@ -1,5 +1,5 @@
{ {
"id": "f7541b9a-140c-4795-a672-4d0c80b56ef1", "id": "6010a064-33aa-404b-b933-c4ca1f1c415d",
"prevId": "00000000-0000-0000-0000-000000000000", "prevId": "00000000-0000-0000-0000-000000000000",
"version": "7", "version": "7",
"dialect": "postgresql", "dialect": "postgresql",
@@ -128,6 +128,12 @@
"primaryKey": true, "primaryKey": true,
"notNull": true "notNull": true
}, },
"slug": {
"name": "slug",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true
},
"title": { "title": {
"name": "title", "name": "title",
"type": "varchar(256)", "type": "varchar(256)",
@@ -172,7 +178,38 @@
"notNull": false "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": { "foreignKeys": {
"images_created_by_users_id_fk": { "images_created_by_users_id_fk": {
"name": "images_created_by_users_id_fk", "name": "images_created_by_users_id_fk",
@@ -189,7 +226,15 @@
} }
}, },
"compositePrimaryKeys": {}, "compositePrimaryKeys": {},
"uniqueConstraints": {} "uniqueConstraints": {
"images_slug_unique": {
"name": "images_slug_unique",
"nullsNotDistinct": false,
"columns": [
"slug"
]
}
}
}, },
"public.sessions": { "public.sessions": {
"name": "sessions", "name": "sessions",

View File

@@ -5,8 +5,8 @@
{ {
"idx": 0, "idx": 0,
"version": "7", "version": "7",
"when": 1726662474693, "when": 1726843311417,
"tag": "0000_wandering_steve_rogers", "tag": "0000_burly_katie_power",
"breakpoints": true "breakpoints": true
} }
] ]

View File

@@ -14,6 +14,9 @@ const config = {
{ {
hostname: "opengifame.dev.fergl.ie", hostname: "opengifame.dev.fergl.ie",
}, },
{
hostname: "cloudflare-ipfs.com",
},
{ {
hostname: "localhost", hostname: "localhost",
}, },

View File

@@ -47,6 +47,7 @@
"@radix-ui/react-toggle": "^1.1.0", "@radix-ui/react-toggle": "^1.1.0",
"@radix-ui/react-toggle-group": "^1.1.0", "@radix-ui/react-toggle-group": "^1.1.0",
"@radix-ui/react-tooltip": "^1.1.2", "@radix-ui/react-tooltip": "^1.1.2",
"@sindresorhus/slugify": "^2.2.1",
"@t3-oss/env-nextjs": "^0.11.1", "@t3-oss/env-nextjs": "^0.11.1",
"@tanstack/react-query": "^5.56.2", "@tanstack/react-query": "^5.56.2",
"@trpc/client": "^11.0.0-rc.446", "@trpc/client": "^11.0.0-rc.446",

12
scripts/scaffold.sql Normal file
View File

@@ -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');

View File

@@ -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 <div>Me so imagey {params.slug}</div>;
};
export default ImageRoute;

View File

@@ -0,0 +1,12 @@
import React from "react";
import { Post } from "@/lib/models/post";
type ImagePageProps = {
post: Post;
};
const ImagePage: React.FC<ImagePageProps> = ({ post }) => {
return <div>{post.title}</div>;
};
export default ImagePage;

View File

@@ -15,7 +15,6 @@ const UserNavDropdown: React.FC<IUserNavDropdownProps> = ({ session }) => {
session?.user?.image || "/images/default-profile.jpg", session?.user?.image || "/images/default-profile.jpg",
); );
React.useEffect(() => { React.useEffect(() => {
logger.debug("UserNavDropdown", "session", session);
setProfileImage(session?.user?.image || "/images/default-profile.jpg"); setProfileImage(session?.user?.image || "/images/default-profile.jpg");
}, [session]); }, [session]);

8
src/lib/models/post.ts Normal file
View File

@@ -0,0 +1,8 @@
export type Post = {
slug: string;
title: string;
description: string;
likes: number;
dislikes: number;
datePosted: Date;
}

View File

@@ -3,17 +3,34 @@ import {
protectedProcedure, protectedProcedure,
publicProcedure, publicProcedure,
} from "@/server/api/trpc"; } from "@/server/api/trpc";
import { slugifyWithCounter } from "@sindresorhus/slugify";
import { z } from "zod"; import { z } from "zod";
import { images } from "@/server/db/schema"; import { images, users } from "@/server/db/schema";
import { env } from "@/env"; import { env } from "@/env";
import { and, eq } from "drizzle-orm";
const imageCreateType = {
title: z.string().min(5),
description: z.string(),
tags: z.array(z.string()),
};
export const imageRouter = createTRPCRouter({ 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 }) => { getTrending: publicProcedure.query(async ({ ctx }) => {
const trending = await ctx.db.query.images.findMany({ const trending = await ctx.db.query.images.findMany({
orderBy: (images, { desc }) => [desc(images.createdAt)], orderBy: (images, { desc }) => [desc(images.createdAt)],
@@ -31,12 +48,34 @@ export const imageRouter = createTRPCRouter({
); );
}), }),
create: protectedProcedure 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 }) => { .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 const post = await ctx.db
.insert(images) .insert(images)
.values({ .values({
title: input.title, title: input.title,
slug: slug,
description: input.description, description: input.description,
tags: input.tags, tags: input.tags,
createdById: ctx.session.user.id, createdById: ctx.session.user.id,

View File

@@ -13,25 +13,35 @@ import { type AdapterAccount } from "next-auth/adapters";
export const createTable = pgTableCreator((name) => `${name}`); export const createTable = pgTableCreator((name) => `${name}`);
export const images = createTable("images", { export const images = createTable(
id: varchar("id", { length: 255 }) "images",
.notNull() {
.primaryKey() id: varchar("id", { length: 255 })
.$defaultFn(() => crypto.randomUUID()), .notNull()
title: varchar("title", { length: 256 }), .primaryKey()
description: varchar("description"), .$defaultFn(() => crypto.randomUUID()),
tags: text("tags").array(), slug: varchar("slug", { length: 255 }).notNull().unique(),
filePath: varchar("filepath", { length: 256 }), title: varchar("title", { length: 256 }),
createdById: varchar("created_by", { length: 255 }) description: varchar("description"),
.notNull() tags: text("tags").array(),
.references(() => users.id), filePath: varchar("filepath", { length: 256 }),
createdAt: timestamp("created_at", { withTimezone: true }) createdById: varchar("created_by", { length: 255 })
.default(sql`CURRENT_TIMESTAMP`) .notNull()
.notNull(), .references(() => users.id),
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate( createdAt: timestamp("created_at", { withTimezone: true })
() => new Date(), .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", { export const users = createTable("users", {
id: varchar("id", { length: 255 }) id: varchar("id", { length: 255 })