mirror of
https://github.com/fergalmoran/opengifame.git
synced 2025-12-22 17:49:18 +00:00
Merge branch '@feature/image_page' into develop
This commit is contained in:
@@ -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");
|
||||||
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ const config = {
|
|||||||
{
|
{
|
||||||
hostname: "opengifame.dev.fergl.ie",
|
hostname: "opengifame.dev.fergl.ie",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
hostname: "cloudflare-ipfs.com",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
hostname: "localhost",
|
hostname: "localhost",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
12
scripts/scaffold.sql
Normal 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');
|
||||||
12
src/app/(site)/(app)/post/[slug]/page.tsx
Normal file
12
src/app/(site)/(app)/post/[slug]/page.tsx
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
import PostPage from "@/components/pages/post/post-page";
|
||||||
|
import { api } from "@/trpc/server";
|
||||||
|
|
||||||
|
type PostRouteParams = {
|
||||||
|
slug: string;
|
||||||
|
};
|
||||||
|
const PostRoute = async ({ params }: { params: PostRouteParams }) => {
|
||||||
|
const post = await api.post.getBySlug({ slug: params.slug });
|
||||||
|
return <PostPage post={post} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostRoute;
|
||||||
@@ -5,7 +5,7 @@ import { api, HydrateClient } from "@/trpc/server";
|
|||||||
|
|
||||||
export default async function Home() {
|
export default async function Home() {
|
||||||
const session = await getServerAuthSession();
|
const session = await getServerAuthSession();
|
||||||
void api.image.getTrending.prefetch();
|
void api.post.getTrending.prefetch();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<HydrateClient>
|
<HydrateClient>
|
||||||
|
|||||||
29
src/components/pages/post/post-page.tsx
Normal file
29
src/components/pages/post/post-page.tsx
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import React from "react";
|
||||||
|
import { Post } from "@/lib/models/post";
|
||||||
|
|
||||||
|
type PostPageProps = {
|
||||||
|
post: Post;
|
||||||
|
};
|
||||||
|
|
||||||
|
const PostPage: React.FC<PostPageProps> = ({ post }) => {
|
||||||
|
return (
|
||||||
|
<div className="grid grid-cols-3">
|
||||||
|
<div id="left"></div>
|
||||||
|
<div id="centre">
|
||||||
|
<div className="flex max-w-lg flex-col justify-center">
|
||||||
|
<img
|
||||||
|
src={post.imageUrl}
|
||||||
|
className="w-full overflow-hidden rounded-lg border shadow"
|
||||||
|
/>
|
||||||
|
<div className="mt-8">
|
||||||
|
<h4 className="text-xl font-bold">{post.title}</h4>
|
||||||
|
<p className="mt-2 text-gray-600">{post.description}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="right"></div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PostPage;
|
||||||
@@ -52,7 +52,7 @@ const UploadPage: React.FC = () => {
|
|||||||
// )
|
// )
|
||||||
// .refine((file) => file.size <= MAX_FILE_SIZE, `Max file size is 5MB.`),
|
// .refine((file) => file.size <= MAX_FILE_SIZE, `Max file size is 5MB.`),
|
||||||
// });
|
// });
|
||||||
const createImage = api.image.create.useMutation({
|
const createImage = api.post.create.useMutation({
|
||||||
onSuccess: async (e) => {
|
onSuccess: async (e) => {
|
||||||
console.log("upload-page", "onSuccess", e);
|
console.log("upload-page", "onSuccess", e);
|
||||||
const file = form.getValues().image;
|
const file = form.getValues().image;
|
||||||
@@ -64,7 +64,7 @@ const UploadPage: React.FC = () => {
|
|||||||
body,
|
body,
|
||||||
});
|
});
|
||||||
if (response.status === StatusCodes.CREATED) {
|
if (response.status === StatusCodes.CREATED) {
|
||||||
await utils.image.invalidate();
|
await utils.post.invalidate();
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}
|
}
|
||||||
logger.error("upload-page", "createImage", response.statusText);
|
logger.error("upload-page", "createImage", response.statusText);
|
||||||
|
|||||||
@@ -15,17 +15,17 @@ const downvote = async () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const TrendingImages: React.FC = async () => {
|
export const TrendingImages: React.FC = async () => {
|
||||||
const latestImages = await api.image.getTrending();
|
const latestImages = await api.post.getTrending();
|
||||||
return latestImages.length !== 0 ? (
|
return latestImages.length !== 0 ? (
|
||||||
<div className="masonry sm:masonry-sm md:masonry-md">
|
<div className="masonry sm:masonry-sm md:masonry-md">
|
||||||
{latestImages.map((image) => (
|
{latestImages.map((image) => (
|
||||||
<Link href={`/images/${image.id}`} key={image.id}>
|
<Link href={`/post/${image.slug}`} key={image.slug}>
|
||||||
<div className="break-inside rounded-lg">
|
<div className="break-inside rounded-lg">
|
||||||
<div className="relative m-6 flex-shrink-0 overflow-hidden rounded-lg bg-muted shadow-lg hover:bg-accent">
|
<div className="relative m-6 flex-shrink-0 overflow-hidden rounded-lg bg-muted shadow-lg hover:bg-accent">
|
||||||
<div className="relative flex items-center justify-center px-2 pt-4">
|
<div className="relative flex items-center justify-center px-2 pt-4">
|
||||||
<img
|
<img
|
||||||
className="h-auto max-w-full rounded-lg"
|
className="h-auto max-w-full rounded-lg"
|
||||||
src={image.url}
|
src={image.imageUrl}
|
||||||
alt={image.description ?? "An image"}
|
alt={image.description ?? "An image"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|
||||||
|
|||||||
10
src/lib/models/post.ts
Normal file
10
src/lib/models/post.ts
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
export type Post = {
|
||||||
|
slug: string;
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
imageUrl: string;
|
||||||
|
likes: number;
|
||||||
|
tags: string[];
|
||||||
|
dislikes: number;
|
||||||
|
datePosted: Date;
|
||||||
|
};
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
import { createCallerFactory, createTRPCRouter } from "@/server/api/trpc";
|
||||||
import { authRouter } from "./routers/auth";
|
import { authRouter } from "@/server/api/routers/auth";
|
||||||
import { imageRouter } from "@/server/api/routers/image";
|
import { postRouter } from "@/server/api/routers/post";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This is the primary router for your server.
|
* This is the primary router for your server.
|
||||||
@@ -9,7 +9,7 @@ import { imageRouter } from "@/server/api/routers/image";
|
|||||||
*/
|
*/
|
||||||
export const appRouter = createTRPCRouter({
|
export const appRouter = createTRPCRouter({
|
||||||
auth: authRouter,
|
auth: authRouter,
|
||||||
image: imageRouter,
|
post: postRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
import {
|
|
||||||
createTRPCRouter,
|
|
||||||
protectedProcedure,
|
|
||||||
publicProcedure,
|
|
||||||
} from "@/server/api/trpc";
|
|
||||||
import { z } from "zod";
|
|
||||||
import { images } from "@/server/db/schema";
|
|
||||||
import { env } from "@/env";
|
|
||||||
|
|
||||||
const imageCreateType = {
|
|
||||||
title: z.string().min(5),
|
|
||||||
description: z.string(),
|
|
||||||
tags: z.array(z.string()),
|
|
||||||
};
|
|
||||||
|
|
||||||
export const imageRouter = createTRPCRouter({
|
|
||||||
getTrending: publicProcedure.query(async ({ ctx }) => {
|
|
||||||
const trending = await ctx.db.query.images.findMany({
|
|
||||||
orderBy: (images, { desc }) => [desc(images.createdAt)],
|
|
||||||
});
|
|
||||||
return (
|
|
||||||
trending.map((t) => {
|
|
||||||
return {
|
|
||||||
id: t.id,
|
|
||||||
title: t.title,
|
|
||||||
description: t.description,
|
|
||||||
tags: t.tags,
|
|
||||||
url: `${env.IMAGE_BASE_URL}/${t.filePath}`,
|
|
||||||
};
|
|
||||||
}) ?? null
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
create: protectedProcedure
|
|
||||||
.input(z.object(imageCreateType))
|
|
||||||
.mutation(async ({ ctx, input }) => {
|
|
||||||
const post = await ctx.db
|
|
||||||
.insert(images)
|
|
||||||
.values({
|
|
||||||
title: input.title,
|
|
||||||
description: input.description,
|
|
||||||
tags: input.tags,
|
|
||||||
createdById: ctx.session.user.id,
|
|
||||||
})
|
|
||||||
.returning();
|
|
||||||
if (!post[0]) {
|
|
||||||
throw new Error("Failed to create image");
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: post[0].id,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
96
src/server/api/routers/post.ts
Normal file
96
src/server/api/routers/post.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from "@/server/api/trpc";
|
||||||
|
import { slugifyWithCounter } from "@sindresorhus/slugify";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { images, users } from "@/server/db/schema";
|
||||||
|
import { env } from "@/env";
|
||||||
|
import { and, eq } from "drizzle-orm";
|
||||||
|
import { Post } from "@/lib/models/post";
|
||||||
|
|
||||||
|
export const postRouter = 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 {
|
||||||
|
slug: image.id,
|
||||||
|
title: image.title,
|
||||||
|
description: image.description,
|
||||||
|
tags: image.tags,
|
||||||
|
imageUrl: `${env.IMAGE_BASE_URL}/${image.filePath}`,
|
||||||
|
likes: 0,
|
||||||
|
dislikes: 0,
|
||||||
|
} as Post;
|
||||||
|
}),
|
||||||
|
getTrending: publicProcedure.query(async ({ ctx }) => {
|
||||||
|
const trending = await ctx.db.query.images.findMany({
|
||||||
|
orderBy: (images, { desc }) => [desc(images.createdAt)],
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
trending.map((t) => {
|
||||||
|
return {
|
||||||
|
slug: t.slug,
|
||||||
|
title: t.title,
|
||||||
|
description: t.description,
|
||||||
|
tags: t.tags,
|
||||||
|
imageUrl: `${env.IMAGE_BASE_URL}/${t.filePath}`,
|
||||||
|
likes: 0,
|
||||||
|
dislikes: 0,
|
||||||
|
} as Post;
|
||||||
|
}) ?? null
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
create: protectedProcedure
|
||||||
|
.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,
|
||||||
|
})
|
||||||
|
.returning();
|
||||||
|
if (!post[0]) {
|
||||||
|
throw new Error("Failed to create image");
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: post[0].id,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -13,11 +13,14 @@ 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(
|
||||||
|
"images",
|
||||||
|
{
|
||||||
id: varchar("id", { length: 255 })
|
id: varchar("id", { length: 255 })
|
||||||
.notNull()
|
.notNull()
|
||||||
.primaryKey()
|
.primaryKey()
|
||||||
.$defaultFn(() => crypto.randomUUID()),
|
.$defaultFn(() => crypto.randomUUID()),
|
||||||
|
slug: varchar("slug", { length: 255 }).notNull().unique(),
|
||||||
title: varchar("title", { length: 256 }),
|
title: varchar("title", { length: 256 }),
|
||||||
description: varchar("description"),
|
description: varchar("description"),
|
||||||
tags: text("tags").array(),
|
tags: text("tags").array(),
|
||||||
@@ -31,7 +34,14 @@ export const images = createTable("images", {
|
|||||||
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
|
updatedAt: timestamp("updated_at", { withTimezone: true }).$onUpdate(
|
||||||
() => new Date(),
|
() => 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 })
|
||||||
|
|||||||
Reference in New Issue
Block a user