From 8e13ab37ffca32cc41c5813d9eb798b22103fd21 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sat, 5 Apr 2025 00:40:26 +0600 Subject: [PATCH 01/32] Refactor ArticleEditor component to use updated autosizing hook and improve textarea handling. Replace useAutoResizeTextarea with useAutosizeTextArea, enhancing functionality with initial height support. Update textarea properties for better user experience. --- src/components/Editor/ArticleEditor.tsx | 34 +++----- src/components/ui/auto.tsx | 109 ++++++++++++++++++++++++ src/hooks/use-auto-resize-textarea.ts | 46 +++++----- 3 files changed, 146 insertions(+), 43 deletions(-) create mode 100644 src/components/ui/auto.tsx diff --git a/src/components/Editor/ArticleEditor.tsx b/src/components/Editor/ArticleEditor.tsx index 90e35b0..c3c6ef1 100644 --- a/src/components/Editor/ArticleEditor.tsx +++ b/src/components/Editor/ArticleEditor.tsx @@ -16,34 +16,21 @@ import { import React, { useRef } from "react"; import { ArticleRepositoryInput } from "@/backend/services/inputs/article.input"; -import { useAutoResizeTextarea } from "@/hooks/use-auto-resize-textarea"; -import { useClickAway } from "@/hooks/use-click-away"; -import { - formattedRelativeTime, - formattedTime, - zodErrorToString, -} from "@/lib/utils"; +import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea"; +import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; +import { useToggle } from "@/hooks/use-toggle"; +import { formattedTime } from "@/lib/utils"; +import { markdownToHtml } from "@/utils/markdoc-parser"; import { useMutation } from "@tanstack/react-query"; import clsx from "clsx"; import Link from "next/link"; import { useRouter } from "next/navigation"; import { useForm } from "react-hook-form"; import { z } from "zod"; -import EditorCommandButton from "./EditorCommandButton"; -import { useMarkdownEditor } from "./useMarkdownEditor"; -import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; -import { markdownToHtml } from "@/utils/markdoc-parser"; import { useAppConfirm } from "../app-confirm"; -import { - Sheet, - SheetContent, - SheetDescription, - SheetHeader, - SheetTitle, - SheetTrigger, -} from "../ui/sheet"; -import { useToggle } from "@/hooks/use-toggle"; import ArticleEditorDrawer from "./ArticleEditorDrawer"; +import EditorCommandButton from "./EditorCommandButton"; +import { useMarkdownEditor } from "./useMarkdownEditor"; interface Prop { uuid?: string; @@ -55,9 +42,8 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { const router = useRouter(); const [isOpenSettingDrawer, toggleSettingDrawer] = useToggle(); const appConfig = useAppConfirm(); - const titleRef = useRef(null); + const titleRef = useRef(null!); const bodyRef = useRef(null); - useAutoResizeTextarea(titleRef); const setDebouncedTitle = useDebouncedCallback(() => handleSaveTitle(), 1000); const setDebouncedBody = useDebouncedCallback(() => handleSaveBody(), 1000); @@ -72,6 +58,8 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { resolver: zodResolver(ArticleRepositoryInput.updateArticleInput), }); + useAutosizeTextArea(titleRef, editorForm.watch("title") ?? ""); + const editor = useMarkdownEditor({ ref: bodyRef }); const updateMyArticleMutation = useMutation({ @@ -226,6 +214,7 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { placeholder={_t("Title")} tabIndex={1} autoFocus + rows={1} value={editorForm.watch("title")} className="w-full text-2xl focus:outline-none bg-background resize-none" ref={titleRef} @@ -257,7 +246,6 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { /> - {/* Editor Textarea */}
{editorMode === "write" ? ( diff --git a/src/components/ui/auto.tsx b/src/components/ui/auto.tsx new file mode 100644 index 0000000..1379aee --- /dev/null +++ b/src/components/ui/auto.tsx @@ -0,0 +1,109 @@ +"use client"; +import * as React from "react"; +import { cn } from "@/lib/utils"; +import { useImperativeHandle } from "react"; + +interface UseAutosizeTextAreaProps { + textAreaRef: React.MutableRefObject; + minHeight?: number; + maxHeight?: number; + triggerAutoSize: string; +} + +export const useAutosizeTextArea = ({ + textAreaRef, + triggerAutoSize, + maxHeight = Number.MAX_SAFE_INTEGER, + minHeight = 0, +}: UseAutosizeTextAreaProps) => { + const [init, setInit] = React.useState(true); + React.useEffect(() => { + // We need to reset the height momentarily to get the correct scrollHeight for the textarea + const offsetBorder = 6; + const textAreaElement = textAreaRef.current; + if (textAreaElement) { + if (init) { + textAreaElement.style.minHeight = `${minHeight + offsetBorder}px`; + if (maxHeight > minHeight) { + textAreaElement.style.maxHeight = `${maxHeight}px`; + } + setInit(false); + } + textAreaElement.style.height = `${minHeight + offsetBorder}px`; + const scrollHeight = textAreaElement.scrollHeight; + // We then set the height directly, outside of the render loop + // Trying to set this with state or a ref will product an incorrect value. + if (scrollHeight > maxHeight) { + textAreaElement.style.height = `${maxHeight}px`; + } else { + textAreaElement.style.height = `${scrollHeight + offsetBorder}px`; + } + } + }, [textAreaRef.current, triggerAutoSize]); +}; + +export type AutosizeTextAreaRef = { + textArea: HTMLTextAreaElement; + maxHeight: number; + minHeight: number; +}; + +type AutosizeTextAreaProps = { + maxHeight?: number; + minHeight?: number; +} & React.TextareaHTMLAttributes; + +export const AutosizeTextarea = React.forwardRef< + AutosizeTextAreaRef, + AutosizeTextAreaProps +>( + ( + { + maxHeight = Number.MAX_SAFE_INTEGER, + minHeight = 52, + className, + onChange, + value, + ...props + }: AutosizeTextAreaProps, + ref: React.Ref + ) => { + const textAreaRef = React.useRef(null); + const [triggerAutoSize, setTriggerAutoSize] = React.useState(""); + + useAutosizeTextArea({ + textAreaRef, + triggerAutoSize: triggerAutoSize, + maxHeight, + minHeight, + }); + + useImperativeHandle(ref, () => ({ + textArea: textAreaRef.current as HTMLTextAreaElement, + focus: () => textAreaRef?.current?.focus(), + maxHeight, + minHeight, + })); + + React.useEffect(() => { + setTriggerAutoSize(value as string); + }, [props?.defaultValue, value]); + + return ( + ) : (
; + onChange?: (value: string) => void; } export function useMarkdownEditor(options?: Options) { @@ -23,8 +24,6 @@ export function useMarkdownEditor(options?: Options) { const { selectionStart, selectionEnd } = textareaRef.current; let updatedValue = textareaRef.current.value; - console.log({ updatedValue, selectionStart, selectionEnd }); - switch (command) { case "heading": updatedValue = @@ -52,9 +51,10 @@ export function useMarkdownEditor(options?: Options) { break; } textareaRef.current.value = updatedValue; - // Trigger input event to notify changes - const event = new Event("change", { bubbles: true }); - textareaRef.current.dispatchEvent(event); + + if (options?.onChange) { + options.onChange(updatedValue); + } textareaRef.current.focus(); }; From f6db89f0a0e1347a1456057091dd500e6ba25956 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sat, 5 Apr 2025 19:42:50 +0600 Subject: [PATCH 16/32] Update ArticleCard component to prepend '@' to author usernames in article URLs and replace anchor tags with Link component for improved routing. --- src/components/ArticleCard.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/ArticleCard.tsx b/src/components/ArticleCard.tsx index e158583..61fc423 100644 --- a/src/components/ArticleCard.tsx +++ b/src/components/ArticleCard.tsx @@ -41,7 +41,7 @@ const ArticleCard = ({ const { lang } = useTranslation(); const articleUrl = useMemo(() => { - return `/${author.username}/${handle}`; + return `/@${author.username}/${handle}`; }, [author.username, handle]); return ( @@ -95,7 +95,7 @@ const ArticleCard = ({
{coverImage && ( - +
-
+ )}
{/*
From 33afb9e5f64bb2bc8472d83a50d7f56b5357e1fa Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sat, 5 Apr 2025 19:43:43 +0600 Subject: [PATCH 17/32] Refactor ArticleCard component to enhance author username display by ensuring consistent '@' prefix in article URLs and improve routing with Link component. --- .caddy-label | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .caddy-label diff --git a/.caddy-label b/.caddy-label new file mode 100644 index 0000000..e7576e3 --- /dev/null +++ b/.caddy-label @@ -0,0 +1,14 @@ +# Primary (www) domain with reverse proxy +caddy_0=https://www.techdiary.dev +caddy_0.handle_path=/* +caddy_0.handle_path.0_reverse_proxy={{upstreams 3000}} +caddy_0.encode=zstd gzip +caddy_0.try_files={path} /index.html /index.php +caddy_0.header=-Server + +# Redirect non-www to www +caddy_1=https://techdiary.dev +caddy_1.redir=https://www.techdiary.dev{uri} + +# Set Docker network for Caddy to access the app +caddy_ingress_network=coolify \ No newline at end of file From b0f4626e9be468c7e4a16069adb57af0b6c53080 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sat, 5 Apr 2025 21:27:29 +0600 Subject: [PATCH 18/32] Add series and series items models, repositories, and database schema - Introduced Series and SeriesItem interfaces in domain models. - Updated persistence repositories to include series and series items. - Added corresponding database schema for series and series items. - Enhanced journal migration with new entries for reflective masque, pink selene, and small yellow claw. --- migrations/0025_reflective_masque.sql | 23 + migrations/0026_pink_selene.sql | 1 + migrations/0027_small_yellow_claw.sql | 1 + migrations/meta/0025_snapshot.json | 810 +++++++++++++++++ migrations/meta/0026_snapshot.json | 816 ++++++++++++++++++ migrations/meta/0027_snapshot.json | 816 ++++++++++++++++++ migrations/meta/_journal.json | 21 + src/app/series/page.tsx | 17 + src/backend/models/domain-models.ts | 23 + src/backend/persistence-repositories.ts | 18 +- .../persistence/persistence.repository.ts | 4 +- src/backend/schemas/schemas.ts | 29 + src/backend/services/inputs/series.input.ts | 8 + src/backend/services/series.action.ts | 65 ++ 14 files changed, 2649 insertions(+), 3 deletions(-) create mode 100644 migrations/0025_reflective_masque.sql create mode 100644 migrations/0026_pink_selene.sql create mode 100644 migrations/0027_small_yellow_claw.sql create mode 100644 migrations/meta/0025_snapshot.json create mode 100644 migrations/meta/0026_snapshot.json create mode 100644 migrations/meta/0027_snapshot.json create mode 100644 src/app/series/page.tsx create mode 100644 src/backend/services/inputs/series.input.ts create mode 100644 src/backend/services/series.action.ts diff --git a/migrations/0025_reflective_masque.sql b/migrations/0025_reflective_masque.sql new file mode 100644 index 0000000..2244eb4 --- /dev/null +++ b/migrations/0025_reflective_masque.sql @@ -0,0 +1,23 @@ +CREATE TABLE "series_items" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "series_id" uuid NOT NULL, + "type" varchar NOT NULL, + "title" varchar, + "article_id" uuid, + "index" integer DEFAULT 0 NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); +--> statement-breakpoint +CREATE TABLE "series" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "title" varchar NOT NULL, + "cover_image" jsonb, + "owner_id" uuid NOT NULL, + "created_at" timestamp, + "updated_at" timestamp +); +--> statement-breakpoint +ALTER TABLE "series_items" ADD CONSTRAINT "series_items_series_id_series_id_fk" FOREIGN KEY ("series_id") REFERENCES "public"."series"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "series_items" ADD CONSTRAINT "series_items_article_id_articles_id_fk" FOREIGN KEY ("article_id") REFERENCES "public"."articles"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "series" ADD CONSTRAINT "series_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action; \ No newline at end of file diff --git a/migrations/0026_pink_selene.sql b/migrations/0026_pink_selene.sql new file mode 100644 index 0000000..2129890 --- /dev/null +++ b/migrations/0026_pink_selene.sql @@ -0,0 +1 @@ +ALTER TABLE "series" ADD COLUMN "handle" varchar NOT NULL; \ No newline at end of file diff --git a/migrations/0027_small_yellow_claw.sql b/migrations/0027_small_yellow_claw.sql new file mode 100644 index 0000000..ac8da34 --- /dev/null +++ b/migrations/0027_small_yellow_claw.sql @@ -0,0 +1 @@ +ALTER TABLE "series" ALTER COLUMN "handle" DROP NOT NULL; \ No newline at end of file diff --git a/migrations/meta/0025_snapshot.json b/migrations/meta/0025_snapshot.json new file mode 100644 index 0000000..80757cd --- /dev/null +++ b/migrations/meta/0025_snapshot.json @@ -0,0 +1,810 @@ +{ + "id": "a2fe20d0-ec12-43aa-8971-fbd5df25d3c7", + "prevId": "9cfc84e0-3085-4499-906f-81b5dd9f1a35", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.article_tag": { + "name": "article_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "article_tag_article_id_articles_id_fk": { + "name": "article_tag_article_id_articles_id_fk", + "tableFrom": "article_tag", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_tag_tag_id_tags_id_fk": { + "name": "article_tag_tag_id_tags_id_fk", + "tableFrom": "article_tag", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "excerpt": { + "name": "excerpt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "articles_author_id_users_id_fk": { + "name": "articles_author_id_users_id_fk", + "tableFrom": "articles", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commentable_type": { + "name": "commentable_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "commentable_id": { + "name": "commentable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series_items": { + "name": "series_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_items_series_id_series_id_fk": { + "name": "series_items_series_id_series_id_fk", + "tableFrom": "series_items", + "tableTo": "series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "series_items_article_id_articles_id_fk": { + "name": "series_items_article_id_articles_id_fk", + "tableFrom": "series_items", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series": { + "name": "series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_owner_id_users_id_fk": { + "name": "series_owner_id_users_id_fk", + "tableFrom": "series", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "followee_id": { + "name": "followee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_followee_id_users_id_fk": { + "name": "user_follows_followee_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "followee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sessions": { + "name": "user_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_action_at": { + "name": "last_action_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_sessions_user_id_users_id_fk": { + "name": "user_sessions_user_id_users_id_fk", + "tableFrom": "user_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_socials": { + "name": "user_socials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "service_uid": { + "name": "service_uid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_socials_user_id_users_id_fk": { + "name": "user_socials_user_id_users_id_fk", + "tableFrom": "user_socials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "profile_photo": { + "name": "profile_photo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "education": { + "name": "education", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "designation": { + "name": "designation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "social_links": { + "name": "social_links", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "profile_readme": { + "name": "profile_readme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0026_snapshot.json b/migrations/meta/0026_snapshot.json new file mode 100644 index 0000000..04ca354 --- /dev/null +++ b/migrations/meta/0026_snapshot.json @@ -0,0 +1,816 @@ +{ + "id": "f9898263-c4dd-48dc-b33d-cdd0eb7631cb", + "prevId": "a2fe20d0-ec12-43aa-8971-fbd5df25d3c7", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.article_tag": { + "name": "article_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "article_tag_article_id_articles_id_fk": { + "name": "article_tag_article_id_articles_id_fk", + "tableFrom": "article_tag", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_tag_tag_id_tags_id_fk": { + "name": "article_tag_tag_id_tags_id_fk", + "tableFrom": "article_tag", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "excerpt": { + "name": "excerpt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "articles_author_id_users_id_fk": { + "name": "articles_author_id_users_id_fk", + "tableFrom": "articles", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commentable_type": { + "name": "commentable_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "commentable_id": { + "name": "commentable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series_items": { + "name": "series_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_items_series_id_series_id_fk": { + "name": "series_items_series_id_series_id_fk", + "tableFrom": "series_items", + "tableTo": "series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "series_items_article_id_articles_id_fk": { + "name": "series_items_article_id_articles_id_fk", + "tableFrom": "series_items", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series": { + "name": "series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_owner_id_users_id_fk": { + "name": "series_owner_id_users_id_fk", + "tableFrom": "series", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "followee_id": { + "name": "followee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_followee_id_users_id_fk": { + "name": "user_follows_followee_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "followee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sessions": { + "name": "user_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_action_at": { + "name": "last_action_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_sessions_user_id_users_id_fk": { + "name": "user_sessions_user_id_users_id_fk", + "tableFrom": "user_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_socials": { + "name": "user_socials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "service_uid": { + "name": "service_uid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_socials_user_id_users_id_fk": { + "name": "user_socials_user_id_users_id_fk", + "tableFrom": "user_socials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "profile_photo": { + "name": "profile_photo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "education": { + "name": "education", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "designation": { + "name": "designation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "social_links": { + "name": "social_links", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "profile_readme": { + "name": "profile_readme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0027_snapshot.json b/migrations/meta/0027_snapshot.json new file mode 100644 index 0000000..08612ff --- /dev/null +++ b/migrations/meta/0027_snapshot.json @@ -0,0 +1,816 @@ +{ + "id": "ceec444b-e45d-4ede-986b-d72a60a77b1b", + "prevId": "f9898263-c4dd-48dc-b33d-cdd0eb7631cb", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.article_tag": { + "name": "article_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "article_tag_article_id_articles_id_fk": { + "name": "article_tag_article_id_articles_id_fk", + "tableFrom": "article_tag", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_tag_tag_id_tags_id_fk": { + "name": "article_tag_tag_id_tags_id_fk", + "tableFrom": "article_tag", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "excerpt": { + "name": "excerpt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "articles_author_id_users_id_fk": { + "name": "articles_author_id_users_id_fk", + "tableFrom": "articles", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commentable_type": { + "name": "commentable_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "commentable_id": { + "name": "commentable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series_items": { + "name": "series_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_items_series_id_series_id_fk": { + "name": "series_items_series_id_series_id_fk", + "tableFrom": "series_items", + "tableTo": "series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "series_items_article_id_articles_id_fk": { + "name": "series_items_article_id_articles_id_fk", + "tableFrom": "series_items", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series": { + "name": "series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_owner_id_users_id_fk": { + "name": "series_owner_id_users_id_fk", + "tableFrom": "series", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "followee_id": { + "name": "followee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_followee_id_users_id_fk": { + "name": "user_follows_followee_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "followee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sessions": { + "name": "user_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_action_at": { + "name": "last_action_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_sessions_user_id_users_id_fk": { + "name": "user_sessions_user_id_users_id_fk", + "tableFrom": "user_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_socials": { + "name": "user_socials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "service": { + "name": "service", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "service_uid": { + "name": "service_uid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_socials_user_id_users_id_fk": { + "name": "user_socials_user_id_users_id_fk", + "tableFrom": "user_socials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "profile_photo": { + "name": "profile_photo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "education": { + "name": "education", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "designation": { + "name": "designation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "social_links": { + "name": "social_links", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "profile_readme": { + "name": "profile_readme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index a8a779a..bf0eb4f 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -176,6 +176,27 @@ "when": 1743331413433, "tag": "0024_complex_prodigy", "breakpoints": true + }, + { + "idx": 25, + "version": "7", + "when": 1743862111643, + "tag": "0025_reflective_masque", + "breakpoints": true + }, + { + "idx": 26, + "version": "7", + "when": 1743863252956, + "tag": "0026_pink_selene", + "breakpoints": true + }, + { + "idx": 27, + "version": "7", + "when": 1743863306485, + "tag": "0027_small_yellow_claw", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/app/series/page.tsx b/src/app/series/page.tsx new file mode 100644 index 0000000..1fcbe30 --- /dev/null +++ b/src/app/series/page.tsx @@ -0,0 +1,17 @@ +import { + getSeriesDetailByHandle, + seriesFeed, +} from "@/backend/services/series.action"; +import React from "react"; + +const page = async () => { + const series = await getSeriesDetailByHandle("js"); + + return ( +
+
{JSON.stringify(series, null, 2)}
+
+ ); +}; + +export default page; diff --git a/src/backend/models/domain-models.ts b/src/backend/models/domain-models.ts index 9bf2291..2af8eff 100644 --- a/src/backend/models/domain-models.ts +++ b/src/backend/models/domain-models.ts @@ -67,3 +67,26 @@ export interface Article { created_at: Date; updated_at: Date; } + +export interface Series { + id: string; + title: string; + handle: string; + cover_image?: IServerFile | null; + owner_id: string; + owner?: User | null; + created_at: Date; + updated_at: Date; +} + +export interface SeriesItem { + id: string; + series_id: string; + type: "TITLE" | "ARTICLE"; + title?: string | null; + article_id?: string | null; + article?: Article | null; + index: number; + created_at: Date; + updated_at: Date; +} diff --git a/src/backend/persistence-repositories.ts b/src/backend/persistence-repositories.ts index e8a2865..e94385b 100644 --- a/src/backend/persistence-repositories.ts +++ b/src/backend/persistence-repositories.ts @@ -1,4 +1,11 @@ -import { Article, User, UserSession, UserSocial } from "./models/domain-models"; +import { + Article, + Series, + SeriesItem, + User, + UserSession, + UserSocial, +} from "./models/domain-models"; import { pgClient } from "./persistence/database-drivers/pg.client"; import { PersistentRepository } from "./persistence/persistence.repository"; @@ -17,9 +24,18 @@ export const userSessionRepository = new PersistentRepository( pgClient ); +const seriesRepository = new PersistentRepository("series", pgClient); + +const seriesItemsRepository = new PersistentRepository( + "series_items", + pgClient +); + export const persistenceRepository = { user: userRepository, userSocial: userSocialRepository, userSession: userSessionRepository, article: articleRepository, + series: seriesRepository, + seriesItems: seriesItemsRepository, }; diff --git a/src/backend/persistence/persistence.repository.ts b/src/backend/persistence/persistence.repository.ts index f80b582..77e6eb0 100644 --- a/src/backend/persistence/persistence.repository.ts +++ b/src/backend/persistence/persistence.repository.ts @@ -33,7 +33,7 @@ export class PersistentRepository { const nodes = await this.findRows({ limit: _limit, offset: _offset, - columns: payload?.columns || [], + columns: payload?.columns, where: payload?.where || undefined, orderBy: payload?.orderBy || [], joins: payload?.joins || [], @@ -65,7 +65,7 @@ export class PersistentRepository { const columns = payload.columns ?.map((col) => `${this.tableName}.${col.toString()}`) - .join(",") ?? "*"; + .join(",") ?? `${this.tableName}.*`; const { whereClause, values } = buildWhereClause(payload.where); const orderByClause = buildOrderByClause(payload?.orderBy); const { joinConditionClause, joinSelectClause } = buildJoinClause( diff --git a/src/backend/schemas/schemas.ts b/src/backend/schemas/schemas.ts index 10c93c5..9d1fa4f 100644 --- a/src/backend/schemas/schemas.ts +++ b/src/backend/schemas/schemas.ts @@ -1,6 +1,7 @@ import { AnyPgColumn, boolean, + integer, json, jsonb, pgTable, @@ -11,6 +12,7 @@ import { varchar, } from "drizzle-orm/pg-core"; import { IServerFile } from "../models/domain-models"; +import { index } from "drizzle-orm/gel-core"; export const usersTable = pgTable("users", { id: uuid("id").defaultRandom().primaryKey(), @@ -66,6 +68,33 @@ export const userFollowsTable = pgTable("user_follows", { updated_at: timestamp("updated_at"), }); +export const seriesTable = pgTable("series", { + id: uuid("id").defaultRandom().primaryKey(), + title: varchar("title").notNull(), + handle: varchar("handle"), + cover_image: jsonb("cover_image").$type(), + owner_id: uuid("owner_id") + .notNull() + .references(() => usersTable.id, { onDelete: "cascade" }), + created_at: timestamp("created_at"), + updated_at: timestamp("updated_at"), +}); + +export const seriesItemsTable = pgTable("series_items", { + id: uuid("id").defaultRandom().primaryKey(), + series_id: uuid("series_id") + .notNull() + .references(() => seriesTable.id, { onDelete: "cascade" }), + type: varchar("type").notNull(), // title, article + title: varchar("title"), + article_id: uuid("article_id").references(() => articlesTable.id, { + onDelete: "cascade", + }), + index: integer("index").notNull().default(0), + created_at: timestamp("created_at"), + updated_at: timestamp("updated_at"), +}); + export const articlesTable = pgTable("articles", { id: uuid("id").defaultRandom().primaryKey(), title: varchar("title").notNull(), diff --git a/src/backend/services/inputs/series.input.ts b/src/backend/services/inputs/series.input.ts new file mode 100644 index 0000000..f542f46 --- /dev/null +++ b/src/backend/services/inputs/series.input.ts @@ -0,0 +1,8 @@ +import { z } from "zod"; + +export const SeriesInput = { + seriesFeedInput: z.object({ + page: z.number().min(1).max(100), + limit: z.number().min(1).max(100), + }), +}; diff --git a/src/backend/services/series.action.ts b/src/backend/services/series.action.ts new file mode 100644 index 0000000..47867bc --- /dev/null +++ b/src/backend/services/series.action.ts @@ -0,0 +1,65 @@ +import { z } from "zod"; +import { SeriesInput } from "./inputs/series.input"; +import { handleRepositoryException } from "./RepositoryException"; +import { persistenceRepository } from "../persistence-repositories"; +import { + asc, + desc, + eq, + joinTable, +} from "../persistence/persistence-where-operator"; +import { Article, Series, SeriesItem, User } from "../models/domain-models"; + +export async function seriesFeed( + _input: z.infer +) { + try { + const input = await SeriesInput.seriesFeedInput.parseAsync(_input); + + return persistenceRepository.series.findAllWithPagination({ + limit: input.limit, + page: input.page, + }); + } catch (error) { + handleRepositoryException(error); + } +} + +export const getSeriesDetailByHandle = async (handle: string) => { + try { + const [series] = await persistenceRepository.series.findRows({ + where: eq("handle", handle), + limit: 1, + joins: [ + joinTable({ + as: "owner", + joinTo: "users", + localField: "owner_id", + foreignField: "id", + columns: ["id", "name", "username", "profile_photo"], + }), + ], + }); + + const serieItems = await persistenceRepository.seriesItems.findRows({ + where: eq("series_id", series.id), + orderBy: [asc("index")], + limit: -1, + joins: [ + joinTable({ + as: "article", + joinTo: "articles", + localField: "article_id", + foreignField: "id", + columns: ["id", "title", "handle"], + }), + ], + }); + return { + series, + serieItems, + }; + } catch (error) { + handleRepositoryException(error); + } +}; From 59bd293225626a180281ab6a1d94464cff86a366 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sun, 6 Apr 2025 00:18:03 +0600 Subject: [PATCH 19/32] Refactor markdown processing in profile and article pages to use markdocParser instead of markdownToHtml, enhancing content rendering. Update ArticleEditor to reflect the same change. Remove obsolete markdownToHtml function from utils. --- src/app/[username]/(profile-page)/page.tsx | 4 +- src/app/[username]/[articleHandle]/page.tsx | 46 ++++++++++++++++----- src/components/Editor/ArticleEditor.tsx | 4 +- src/utils/markdoc-parser.ts | 9 ---- src/utils/markdoc-parser.tsx | 31 ++++++++++++++ src/utils/markdown-tags.tsx | 29 +++++++++++++ 6 files changed, 99 insertions(+), 24 deletions(-) delete mode 100644 src/utils/markdoc-parser.ts create mode 100644 src/utils/markdoc-parser.tsx create mode 100644 src/utils/markdown-tags.tsx diff --git a/src/app/[username]/(profile-page)/page.tsx b/src/app/[username]/(profile-page)/page.tsx index 09039d2..e3b5e4f 100644 --- a/src/app/[username]/(profile-page)/page.tsx +++ b/src/app/[username]/(profile-page)/page.tsx @@ -1,6 +1,6 @@ import { getUserByUsername } from "@/backend/services/user.action"; import _t from "@/i18n/_t"; -import { markdownToHtml } from "@/utils/markdoc-parser"; +import { markdocParser } from "@/utils/markdoc-parser"; import Image from "next/image"; import React from "react"; @@ -21,7 +21,7 @@ const UserProfilePage: React.FC = async ({ params }) => { {profile?.profile_readme ? (
diff --git a/src/app/[username]/[articleHandle]/page.tsx b/src/app/[username]/[articleHandle]/page.tsx index 1c1c921..d41e901 100644 --- a/src/app/[username]/[articleHandle]/page.tsx +++ b/src/app/[username]/[articleHandle]/page.tsx @@ -4,17 +4,15 @@ import AppImage from "@/components/AppImage"; import type { Article, WithContext } from "schema-dts"; import HomepageLayout from "@/components/layout/HomepageLayout"; import { readingTime, removeMarkdownSyntax } from "@/lib/utils"; -import { markdownToHtml } from "@/utils/markdoc-parser"; -import { Metadata, NextPage } from "next"; +import { markdocParser } from "@/utils/markdoc-parser"; +import { Metadata, NextPage, ResolvingMetadata } from "next"; import Image from "next/image"; import Link from "next/link"; import { notFound } from "next/navigation"; import ArticleSidebar from "./_components/ArticleSidebar"; import getFileUrl from "@/utils/getFileUrl"; - -export const metadata: Metadata = { - title: "Article detail", -}; +import { persistenceRepository } from "@/backend/persistence-repositories"; +import { eq } from "@/backend/persistence/persistence-where-operator"; interface ArticlePageProps { params: Promise<{ @@ -23,6 +21,35 @@ interface ArticlePageProps { }>; } +export async function generateMetadata( + { params }: ArticlePageProps, + parent: ResolvingMetadata +): Promise { + // read route params + const { articleHandle } = await params; + const [article] = await persistenceRepository.article.findRows({ + where: eq("handle", articleHandle), + columns: ["title", "excerpt", "cover_image", "body"], + limit: 1, + }); + + return { + title: article.title, + description: removeMarkdownSyntax( + article.excerpt ?? article.body ?? "", + 20 + ), + openGraph: { + images: [ + { + url: getFileUrl(article.cover_image), + alt: article.title, + }, + ], + }, + }; +} + const Page: NextPage = async ({ params }) => { const _params = await params; const article = await articleActions.articleDetailByHandle( @@ -48,7 +75,7 @@ const Page: NextPage = async ({ params }) => { throw notFound(); } - const parsedHTML = markdownToHtml(article?.body ?? ""); + const parsedHTML = markdocParser(article?.body ?? ""); return ( <> @@ -117,10 +144,7 @@ const Page: NextPage = async ({ params }) => {

{article?.title ?? ""}

-
+
{parsedHTML}
diff --git a/src/components/Editor/ArticleEditor.tsx b/src/components/Editor/ArticleEditor.tsx index fbb0e7a..8f6b397 100644 --- a/src/components/Editor/ArticleEditor.tsx +++ b/src/components/Editor/ArticleEditor.tsx @@ -20,7 +20,7 @@ import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea"; import { useDebouncedCallback } from "@/hooks/use-debounced-callback"; import { useToggle } from "@/hooks/use-toggle"; import { formattedTime } from "@/lib/utils"; -import { markdownToHtml } from "@/utils/markdoc-parser"; +import { markdocParser } from "@/utils/markdoc-parser"; import { useMutation } from "@tanstack/react-query"; import clsx from "clsx"; import Link from "next/link"; @@ -272,7 +272,7 @@ const ArticleEditor: React.FC = ({ article, uuid }) => {
)} diff --git a/src/utils/markdoc-parser.ts b/src/utils/markdoc-parser.ts deleted file mode 100644 index ba9399d..0000000 --- a/src/utils/markdoc-parser.ts +++ /dev/null @@ -1,9 +0,0 @@ -import Markdoc from "@markdoc/markdoc"; - -export const markdownToHtml = (markdown: string) => { - const ast = Markdoc.parse(markdown); - const content = Markdoc.transform(ast); - const html = Markdoc.renderers.html(content); - - return html; -}; diff --git a/src/utils/markdoc-parser.tsx b/src/utils/markdoc-parser.tsx new file mode 100644 index 0000000..82084f8 --- /dev/null +++ b/src/utils/markdoc-parser.tsx @@ -0,0 +1,31 @@ +import Markdoc from "@markdoc/markdoc"; +import React from "react"; +import { Callout } from "./markdown-tags"; + +export const callout = { + render: "Callout", + children: ["paragraph", "tag", "list"], + attributes: { + type: { + type: String, + default: "note", + matches: ["caution", "check", "note", "warning"], + }, + title: { + type: String, + }, + }, +}; + +export const markdocParser = (markdown: string) => { + const ast = Markdoc.parse(markdown); + const content = Markdoc.transform(ast, { tags: { callout } }); + + Markdoc.renderers.react(content, React, { components: { callout: Callout } }); + return Markdoc.renderers.react(content, React, { + components: { + // The key here is the same string as `tag` in the previous step + Callout: Callout, + }, + }); +}; diff --git a/src/utils/markdown-tags.tsx b/src/utils/markdown-tags.tsx new file mode 100644 index 0000000..eb5d9d7 --- /dev/null +++ b/src/utils/markdown-tags.tsx @@ -0,0 +1,29 @@ +"use client"; + +import React from "react"; + +interface CalloutProps { + type: "note" | "caution" | "check" | "warning"; + title: string; +} + +export function Callout(props: any) { + const [count, setCount] = React.useState(0); + + return ( +
+
+
+ Callout {count} + + +
+            {JSON.stringify(props.children, null, 2)}
+          
+
+
+
+ ); +} From 39042ade8db544a21eea6b4686d7da99dc701d35 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sun, 6 Apr 2025 00:25:08 +0600 Subject: [PATCH 20/32] Remove robots.txt file and refactor profile and article rendering to eliminate the use of dangerouslySetInnerHTML, enhancing security and performance by directly rendering parsed content. --- public/robots.txt | 2 -- src/app/[username]/(profile-page)/page.tsx | 10 +++------- src/app/robots.ts | 12 ++++++++++++ src/components/Editor/ArticleEditor.tsx | 9 +++------ 4 files changed, 18 insertions(+), 15 deletions(-) delete mode 100644 public/robots.txt create mode 100644 src/app/robots.ts diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 6f27bb6..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,2 +0,0 @@ -User-agent: * -Disallow: \ No newline at end of file diff --git a/src/app/[username]/(profile-page)/page.tsx b/src/app/[username]/(profile-page)/page.tsx index e3b5e4f..23c4cca 100644 --- a/src/app/[username]/(profile-page)/page.tsx +++ b/src/app/[username]/(profile-page)/page.tsx @@ -14,17 +14,13 @@ const UserProfilePage: React.FC = async ({ params }) => { : _params.username.toLowerCase(); const profile = await getUserByUsername(username, ["profile_readme"]); - // return
{JSON.stringify(profile, null, 2)}
; return (
{profile?.profile_readme ? ( -
+
+ {markdocParser(profile?.profile_readme ?? "")} +
) : (
= ({ article, uuid }) => { onChange={handleBodyContentChange} > ) : ( -
+
+ {markdocParser(editorForm.watch("body") ?? "")} +
)}
From 6f6c3f752aa62571ef83fb7c1c74cb40d3bb0601 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sun, 6 Apr 2025 00:26:09 +0600 Subject: [PATCH 21/32] Refactor generateMetadata function in ArticlePage to improve parameter handling by renaming and restructuring the input options, enhancing code clarity and maintainability. --- src/app/[username]/[articleHandle]/page.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/[username]/[articleHandle]/page.tsx b/src/app/[username]/[articleHandle]/page.tsx index d41e901..7de0685 100644 --- a/src/app/[username]/[articleHandle]/page.tsx +++ b/src/app/[username]/[articleHandle]/page.tsx @@ -22,11 +22,10 @@ interface ArticlePageProps { } export async function generateMetadata( - { params }: ArticlePageProps, - parent: ResolvingMetadata + options: ArticlePageProps ): Promise { // read route params - const { articleHandle } = await params; + const { articleHandle } = await options.params; const [article] = await persistenceRepository.article.findRows({ where: eq("handle", articleHandle), columns: ["title", "excerpt", "cover_image", "body"], From 4c3e0668689981a791719b9c3cbc94395f1ca025 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sun, 6 Apr 2025 00:27:37 +0600 Subject: [PATCH 22/32] Add fallback for article metadata generation in Page component to handle cases without a cover image, ensuring consistent title and description output. --- src/app/[username]/[articleHandle]/page.tsx | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/app/[username]/[articleHandle]/page.tsx b/src/app/[username]/[articleHandle]/page.tsx index 7de0685..e4df358 100644 --- a/src/app/[username]/[articleHandle]/page.tsx +++ b/src/app/[username]/[articleHandle]/page.tsx @@ -32,6 +32,13 @@ export async function generateMetadata( limit: 1, }); + if (!article.cover_image) { + return { + title: article.title, + description: removeMarkdownSyntax(article.body ?? "", 20), + }; + } + return { title: article.title, description: removeMarkdownSyntax( From 76ef2ab472070c3ff9b272b8045abe8cf4ba5e38 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sun, 6 Apr 2025 01:18:19 +0600 Subject: [PATCH 23/32] Enhance article metadata handling in Page component by adding support for cases without a cover image, ensuring consistent title and description output. --- .../[articleHandle]/opengraph-image.tsx | 114 ++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 src/app/[username]/[articleHandle]/opengraph-image.tsx diff --git a/src/app/[username]/[articleHandle]/opengraph-image.tsx b/src/app/[username]/[articleHandle]/opengraph-image.tsx new file mode 100644 index 0000000..8557ea3 --- /dev/null +++ b/src/app/[username]/[articleHandle]/opengraph-image.tsx @@ -0,0 +1,114 @@ +import { Article, User } from "@/backend/models/domain-models"; +import { persistenceRepository } from "@/backend/persistence-repositories"; +import { + eq, + joinTable, +} from "@/backend/persistence/persistence-where-operator"; +import { ImageResponse } from "next/og"; +import { readFile } from "node:fs/promises"; +import { join } from "node:path"; + +interface ArticlePageProps { + params: Promise<{ + username: string; + articleHandle: string; + }>; +} + +export const size = { + width: 1200, + height: 630, +}; +export const contentType = "image/png"; + +const getFileLocation = async (path: string) => { + const logoData = await readFile(join(process.cwd(), path)); + return Uint8Array.from(logoData).buffer; +}; + +export default async function Image(options: ArticlePageProps) { + const { articleHandle } = await options.params; + const [article] = await persistenceRepository.article.findRows({ + where: eq("handle", articleHandle), + columns: ["title", "excerpt", "cover_image", "body"], + limit: 1, + joins: [ + joinTable({ + as: "user", + joinTo: "users", + localField: "author_id", + foreignField: "id", + columns: ["username", "profile_photo"], + }), + ], + }); + + return new ImageResponse( + ( +
+

+ {article.title} +

+ +
+ {/* User profile */} +
+ logo +

+ @{article.user?.username ?? "Unknown user"} +

+
+ + {/* Logo */} +
+ logo +

Techdiary

+
+
+
+ ), + { + ...size, + fonts: [ + { + name: "KohinoorBangla", + data: await getFileLocation( + "/public/fonts/KohinoorBangla-Regular.woff" + ), + style: "normal", + weight: 400, + }, + ], + } + ); +} From e5c8afdb4c4ade4e355ff1a1e38a53411844b040 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Sun, 6 Apr 2025 01:32:53 +0600 Subject: [PATCH 24/32] Refactor OpenGraph image component to enhance layout and styling by replacing the h2 element with a div for better structure, and adjusting styles for improved visual presentation of the article title. --- .../[articleHandle]/opengraph-image.tsx | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/app/[username]/[articleHandle]/opengraph-image.tsx b/src/app/[username]/[articleHandle]/opengraph-image.tsx index 8557ea3..962d7b7 100644 --- a/src/app/[username]/[articleHandle]/opengraph-image.tsx +++ b/src/app/[username]/[articleHandle]/opengraph-image.tsx @@ -57,14 +57,26 @@ export default async function Image(options: ArticlePageProps) { width: "100%", }} > -

- {article.title} -

+

+ {article.title} +

+
Date: Sun, 6 Apr 2025 01:38:33 +0600 Subject: [PATCH 25/32] Update font size in OpenGraph image component for improved visibility of article title. --- src/app/[username]/[articleHandle]/opengraph-image.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/[username]/[articleHandle]/opengraph-image.tsx b/src/app/[username]/[articleHandle]/opengraph-image.tsx index 962d7b7..7385f18 100644 --- a/src/app/[username]/[articleHandle]/opengraph-image.tsx +++ b/src/app/[username]/[articleHandle]/opengraph-image.tsx @@ -69,7 +69,7 @@ export default async function Image(options: ArticlePageProps) { >

Date: Sun, 6 Apr 2025 00:01:08 -0500 Subject: [PATCH 26/32] typo fix --- .gitignore | 1 + .vscode/launch.json | 35 ++++++++++++++++++++++ src/app/(home)/_components/ArticleFeed.tsx | 3 +- src/backend/services/article.actions.ts | 4 +-- 4 files changed, 40 insertions(+), 3 deletions(-) create mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index f31f6d9..0eb6f40 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ /.pnp .pnp.js yarn.lock +pnpm-lock.yaml .yarn/cache bun.lockb .yarn/install-state.gz diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..725be5a --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,35 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Next.js: debug server-side", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "skipFiles": ["/**"], + "serverReadyAction": { + "pattern": "started server on .+, url: (https?://.+)", + "uriFormat": "%s", + "action": "openExternally" + } + }, + { + "name": "Next.js: debug client-side", + "type": "chrome", + "request": "launch", + "url": "http://localhost:3000", + "webRoot": "${workspaceFolder}" + }, + { + "name": "Next.js: debug full stack", + "type": "node-terminal", + "request": "launch", + "command": "npm run dev", + "serverReadyAction": { + "pattern": "started server on .+, url: (https?://.+)", + "uriFormat": "%s", + "action": "debugWithChrome" + } + } + ] +} diff --git a/src/app/(home)/_components/ArticleFeed.tsx b/src/app/(home)/_components/ArticleFeed.tsx index c165058..64e91e6 100644 --- a/src/app/(home)/_components/ArticleFeed.tsx +++ b/src/app/(home)/_components/ArticleFeed.tsx @@ -2,10 +2,10 @@ import * as articleActions from "@/backend/services/article.actions"; import ArticleCard from "@/components/ArticleCard"; +import VisibilitySensor from "@/components/VisibilitySensor"; import { readingTime } from "@/lib/utils"; import getFileUrl from "@/utils/getFileUrl"; import { useInfiniteQuery } from "@tanstack/react-query"; -import VisibilitySensor from "@/components/VisibilitySensor"; const ArticleFeed = () => { const feedInfiniteQuery = useInfiniteQuery({ @@ -14,6 +14,7 @@ const ArticleFeed = () => { articleActions.articleFeed({ limit: 5, page: pageParam }), initialPageParam: 1, getNextPageParam: (lastPage) => { + if (!lastPage?.meta.hasNextPage) return undefined; const _page = lastPage?.meta?.currentPage ?? 1; return _page + 1; }, diff --git a/src/backend/services/article.actions.ts b/src/backend/services/article.actions.ts index d7f7c54..2c24599 100644 --- a/src/backend/services/article.actions.ts +++ b/src/backend/services/article.actions.ts @@ -1,6 +1,7 @@ "use server"; -import { generateRandomString, removeMarkdownSyntax } from "@/lib/utils"; +import { slugify } from "@/lib/slug-helper.util"; +import { removeMarkdownSyntax } from "@/lib/utils"; import { z } from "zod"; import { Article, User } from "../models/domain-models"; import { pgClient } from "../persistence/database-drivers/pg.client"; @@ -18,7 +19,6 @@ import { } from "./RepositoryException"; import { ArticleRepositoryInput } from "./inputs/article.input"; import { getSessionUserId } from "./session.actions"; -import { slugify } from "@/lib/slug-helper.util"; const articleRepository = new PersistentRepository
( "articles", From eed0c5aa781d7c65c7217dfdd1a33d2fba807135 Mon Sep 17 00:00:00 2001 From: Shoaib Sharif Date: Sun, 6 Apr 2025 01:13:30 -0500 Subject: [PATCH 27/32] feat(migrations): update user_socials and user_follows tables to use UUIDs for IDs - Altered the "user_socials" table to change the "id" column type to UUID and set the default to generate a random UUID. - Altered the "user_follows" table to change the "id" column type to UUID and set the default to generate a random UUID. - Updated migration snapshots to reflect changes in the database schema. --- migrations/0028_redundant_landau.sql | 2 + migrations/0029_blushing_roland_deschain.sql | 2 + migrations/meta/0028_snapshot.json | 817 ++++++++++++++++++ migrations/meta/0029_snapshot.json | 818 +++++++++++++++++++ migrations/meta/_journal.json | 14 + src/backend/schemas/schemas.ts | 5 +- src/backend/services/user.repository.ts | 2 +- src/store/i18n-lang.atom.ts | 2 +- 8 files changed, 1657 insertions(+), 5 deletions(-) create mode 100644 migrations/0028_redundant_landau.sql create mode 100644 migrations/0029_blushing_roland_deschain.sql create mode 100644 migrations/meta/0028_snapshot.json create mode 100644 migrations/meta/0029_snapshot.json diff --git a/migrations/0028_redundant_landau.sql b/migrations/0028_redundant_landau.sql new file mode 100644 index 0000000..a910e91 --- /dev/null +++ b/migrations/0028_redundant_landau.sql @@ -0,0 +1,2 @@ +ALTER TABLE "user_socials" ALTER COLUMN "id" SET DATA TYPE uuid;--> statement-breakpoint +ALTER TABLE "user_socials" ALTER COLUMN "id" SET DEFAULT gen_random_uuid(); \ No newline at end of file diff --git a/migrations/0029_blushing_roland_deschain.sql b/migrations/0029_blushing_roland_deschain.sql new file mode 100644 index 0000000..bed12b1 --- /dev/null +++ b/migrations/0029_blushing_roland_deschain.sql @@ -0,0 +1,2 @@ +ALTER TABLE "user_follows" ALTER COLUMN "id" SET DATA TYPE uuid;--> statement-breakpoint +ALTER TABLE "user_follows" ALTER COLUMN "id" SET DEFAULT gen_random_uuid(); \ No newline at end of file diff --git a/migrations/meta/0028_snapshot.json b/migrations/meta/0028_snapshot.json new file mode 100644 index 0000000..0075ffa --- /dev/null +++ b/migrations/meta/0028_snapshot.json @@ -0,0 +1,817 @@ +{ + "id": "d8d27f76-1a27-4e41-9282-f56786e5aa9f", + "prevId": "ceec444b-e45d-4ede-986b-d72a60a77b1b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.article_tag": { + "name": "article_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "article_tag_article_id_articles_id_fk": { + "name": "article_tag_article_id_articles_id_fk", + "tableFrom": "article_tag", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_tag_tag_id_tags_id_fk": { + "name": "article_tag_tag_id_tags_id_fk", + "tableFrom": "article_tag", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "excerpt": { + "name": "excerpt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "articles_author_id_users_id_fk": { + "name": "articles_author_id_users_id_fk", + "tableFrom": "articles", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commentable_type": { + "name": "commentable_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "commentable_id": { + "name": "commentable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series_items": { + "name": "series_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_items_series_id_series_id_fk": { + "name": "series_items_series_id_series_id_fk", + "tableFrom": "series_items", + "tableTo": "series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "series_items_article_id_articles_id_fk": { + "name": "series_items_article_id_articles_id_fk", + "tableFrom": "series_items", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series": { + "name": "series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_owner_id_users_id_fk": { + "name": "series_owner_id_users_id_fk", + "tableFrom": "series", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "followee_id": { + "name": "followee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_followee_id_users_id_fk": { + "name": "user_follows_followee_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "followee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sessions": { + "name": "user_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_action_at": { + "name": "last_action_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_sessions_user_id_users_id_fk": { + "name": "user_sessions_user_id_users_id_fk", + "tableFrom": "user_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_socials": { + "name": "user_socials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "service": { + "name": "service", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "service_uid": { + "name": "service_uid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_socials_user_id_users_id_fk": { + "name": "user_socials_user_id_users_id_fk", + "tableFrom": "user_socials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "profile_photo": { + "name": "profile_photo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "education": { + "name": "education", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "designation": { + "name": "designation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "social_links": { + "name": "social_links", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "profile_readme": { + "name": "profile_readme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/0029_snapshot.json b/migrations/meta/0029_snapshot.json new file mode 100644 index 0000000..bf2c3b1 --- /dev/null +++ b/migrations/meta/0029_snapshot.json @@ -0,0 +1,818 @@ +{ + "id": "6077cd2c-0b02-495c-84fc-61c88d6272b2", + "prevId": "d8d27f76-1a27-4e41-9282-f56786e5aa9f", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.article_tag": { + "name": "article_tag", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "serial", + "primaryKey": true, + "notNull": true + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag_id": { + "name": "tag_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "article_tag_article_id_articles_id_fk": { + "name": "article_tag_article_id_articles_id_fk", + "tableFrom": "article_tag", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "article_tag_tag_id_tags_id_fk": { + "name": "article_tag_tag_id_tags_id_fk", + "tableFrom": "article_tag", + "tableTo": "tags", + "columnsFrom": [ + "tag_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.articles": { + "name": "articles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "excerpt": { + "name": "excerpt", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_published": { + "name": "is_published", + "type": "boolean", + "primaryKey": false, + "notNull": false, + "default": false + }, + "published_at": { + "name": "published_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "author_id": { + "name": "author_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "articles_author_id_users_id_fk": { + "name": "articles_author_id_users_id_fk", + "tableFrom": "articles", + "tableTo": "users", + "columnsFrom": [ + "author_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.comments": { + "name": "comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "commentable_type": { + "name": "commentable_type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "commentable_id": { + "name": "commentable_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "comments_user_id_users_id_fk": { + "name": "comments_user_id_users_id_fk", + "tableFrom": "comments", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "comments_parent_id_comments_id_fk": { + "name": "comments_parent_id_comments_id_fk", + "tableFrom": "comments", + "tableTo": "comments", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series_items": { + "name": "series_items", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "series_id": { + "name": "series_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "article_id": { + "name": "article_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "index": { + "name": "index", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_items_series_id_series_id_fk": { + "name": "series_items_series_id_series_id_fk", + "tableFrom": "series_items", + "tableTo": "series", + "columnsFrom": [ + "series_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "series_items_article_id_articles_id_fk": { + "name": "series_items_article_id_articles_id_fk", + "tableFrom": "series_items", + "tableTo": "articles", + "columnsFrom": [ + "article_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.series": { + "name": "series", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "title": { + "name": "title", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "handle": { + "name": "handle", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "cover_image": { + "name": "cover_image", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "owner_id": { + "name": "owner_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "series_owner_id_users_id_fk": { + "name": "series_owner_id_users_id_fk", + "tableFrom": "series", + "tableTo": "users", + "columnsFrom": [ + "owner_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.tags": { + "name": "tags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar(50)", + "primaryKey": false, + "notNull": true + }, + "icon": { + "name": "icon", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "varchar(6)", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_follows": { + "name": "user_follows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "follower_id": { + "name": "follower_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "followee_id": { + "name": "followee_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_follows_follower_id_users_id_fk": { + "name": "user_follows_follower_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "follower_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "user_follows_followee_id_users_id_fk": { + "name": "user_follows_followee_id_users_id_fk", + "tableFrom": "user_follows", + "tableTo": "users", + "columnsFrom": [ + "followee_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sessions": { + "name": "user_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "device": { + "name": "device", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "ip": { + "name": "ip", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "last_action_at": { + "name": "last_action_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_sessions_user_id_users_id_fk": { + "name": "user_sessions_user_id_users_id_fk", + "tableFrom": "user_sessions", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_socials": { + "name": "user_socials", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "service": { + "name": "service", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "service_uid": { + "name": "service_uid", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_socials_user_id_users_id_fk": { + "name": "user_socials_user_id_users_id_fk", + "tableFrom": "user_socials", + "tableTo": "users", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.users": { + "name": "users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "username": { + "name": "username", + "type": "varchar", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "profile_photo": { + "name": "profile_photo", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "education": { + "name": "education", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "designation": { + "name": "designation", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "bio": { + "name": "bio", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "website_url": { + "name": "website_url", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "location": { + "name": "location", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "social_links": { + "name": "social_links", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "profile_readme": { + "name": "profile_readme", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skills": { + "name": "skills", + "type": "varchar", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/migrations/meta/_journal.json b/migrations/meta/_journal.json index bf0eb4f..add4234 100644 --- a/migrations/meta/_journal.json +++ b/migrations/meta/_journal.json @@ -197,6 +197,20 @@ "when": 1743863306485, "tag": "0027_small_yellow_claw", "breakpoints": true + }, + { + "idx": 28, + "version": "7", + "when": 1743919426607, + "tag": "0028_redundant_landau", + "breakpoints": true + }, + { + "idx": 29, + "version": "7", + "when": 1743919570807, + "tag": "0029_blushing_roland_deschain", + "breakpoints": true } ] } \ No newline at end of file diff --git a/src/backend/schemas/schemas.ts b/src/backend/schemas/schemas.ts index 9d1fa4f..aa7e6a2 100644 --- a/src/backend/schemas/schemas.ts +++ b/src/backend/schemas/schemas.ts @@ -12,7 +12,6 @@ import { varchar, } from "drizzle-orm/pg-core"; import { IServerFile } from "../models/domain-models"; -import { index } from "drizzle-orm/gel-core"; export const usersTable = pgTable("users", { id: uuid("id").defaultRandom().primaryKey(), @@ -33,7 +32,7 @@ export const usersTable = pgTable("users", { }); export const userSocialsTable = pgTable("user_socials", { - id: serial("id").primaryKey(), + id: uuid("id").defaultRandom().primaryKey(), service: varchar("service").notNull(), service_uid: varchar("service_uid").notNull(), user_id: uuid("user_id") @@ -57,7 +56,7 @@ export const userSessionsTable = pgTable("user_sessions", { }); export const userFollowsTable = pgTable("user_follows", { - id: serial("id").primaryKey(), + id: uuid("id").defaultRandom().primaryKey(), follower_id: uuid("follower_id") .notNull() .references(() => usersTable.id, { onDelete: "cascade" }), diff --git a/src/backend/services/user.repository.ts b/src/backend/services/user.repository.ts index 3474cd5..1ea7a4c 100644 --- a/src/backend/services/user.repository.ts +++ b/src/backend/services/user.repository.ts @@ -48,7 +48,7 @@ export async function bootSocialUser( const [userSocial] = await persistenceRepository.userSocial.findRows({ where: and( eq("service", input.service), - eq("service_uid", input.service) + eq("service_uid", input.service_uid) ), columns: ["id", "service", "service_uid", "user_id"], limit: 1, diff --git a/src/store/i18n-lang.atom.ts b/src/store/i18n-lang.atom.ts index 0f31680..3d09f18 100644 --- a/src/store/i18n-lang.atom.ts +++ b/src/store/i18n-lang.atom.ts @@ -1,3 +1,3 @@ import { atom } from "jotai"; -export const i18nLangAtom = atom<"en" | "bn" | null>(null); +export const i18nLangAtom = atom<"en" | "bn">("bn"); From f15242e49317facbef511a07b5693f3630b85caa Mon Sep 17 00:00:00 2001 From: Shoaib Sharif Date: Sun, 6 Apr 2025 02:30:33 -0500 Subject: [PATCH 28/32] Updated session dashboard --- package.json | 1 + src/app/dashboard/sessions/page.tsx | 93 +++++++++++++++-------------- 2 files changed, 50 insertions(+), 44 deletions(-) diff --git a/package.json b/package.json index 480cfae..27d8659 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "cloudinary": "^2.6.0", "clsx": "^2.1.1", "cmdk": "^1.1.1", + "date-fns": "^4.1.0", "dotenv": "^16.4.7", "drizzle-orm": "^0.41.0", "jotai": "^2.12.2", diff --git a/src/app/dashboard/sessions/page.tsx b/src/app/dashboard/sessions/page.tsx index 9ab35ba..b1603e8 100644 --- a/src/app/dashboard/sessions/page.tsx +++ b/src/app/dashboard/sessions/page.tsx @@ -15,6 +15,7 @@ import { Loader, LogOut, } from "lucide-react"; +import { formatDistance } from "date-fns"; const SessionsPage = () => { const authSession = useSession(); @@ -44,52 +45,56 @@ const SessionsPage = () => { )} {sessionQuery.data?.map((session) => ( -
-
-
- - {session.device} -
-
- - - Last active {formattedTime(session.last_action_at!)} - -
- -
IP: {session.ip}
- - {authSession?.session?.id == session.id && ( -
- Current Session +
+ + + +
+ {session.device} + {authSession?.session?.id == session.id ? ( +
This device
+ ) : ( +
+ + + Last active{" "} + {formatDistance( + new Date(session.last_action_at!), + new Date(), + { addSuffix: true } + )} +
)} - +
IP: {session.ip}
+ + +
))} From 8ae4d0622f83da7ded7053165c5557ebd2fda249 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Fri, 11 Apr 2025 02:21:07 +0600 Subject: [PATCH 29/32] Refactor article handle generation logic in article.actions.ts to support unique handles with optional article ID. Update ArticleEditor to use debounced save functions for title and body, improving performance and user experience. Add error display in ArticleEditorDrawer for better form validation feedback. --- src/app/api/play/route.ts | 8 +- src/backend/services/article.actions.ts | 88 +++++++++++++++++-- src/components/Editor/ArticleEditor.tsx | 48 ++++++---- src/components/Editor/ArticleEditorDrawer.tsx | 2 + 4 files changed, 116 insertions(+), 30 deletions(-) diff --git a/src/app/api/play/route.ts b/src/app/api/play/route.ts index dc82ddf..43644d5 100644 --- a/src/app/api/play/route.ts +++ b/src/app/api/play/route.ts @@ -1,9 +1,11 @@ -import { slugify } from "@/lib/slug-helper.util"; +import * as articleActions from "@/backend/services/article.actions"; import { NextResponse } from "next/server"; export async function GET(request: Request) { - // const _headers = await headers(); return NextResponse.json({ - slug: slugify("কেমন আছেন আপনারা?"), + handle: await articleActions.getUniqueArticleHandle( + "untitled", + "fc6cfc91-f017-4923-9706-8813ae8df621" + ), }); } diff --git a/src/backend/services/article.actions.ts b/src/backend/services/article.actions.ts index 2c24599..5abe23e 100644 --- a/src/backend/services/article.actions.ts +++ b/src/backend/services/article.actions.ts @@ -10,7 +10,9 @@ import { desc, eq, joinTable, + like, neq, + or, } from "../persistence/persistence-where-operator"; import { PersistentRepository } from "../persistence/persistence.repository"; import { @@ -84,18 +86,86 @@ export async function createMyArticle( } } -export const getUniqueArticleHandle = async (title: string) => { +export const getUniqueArticleHandle = async ( + title: string, + ignoreArticleId?: string +) => { try { - const count = await articleRepository.findRowCount({ - where: eq("handle", slugify(title)), - columns: ["id", "handle"], + // Slugify the title first + const baseHandle = slugify(title); + + // If we have an ignoreArticleId, check if this article already exists + if (ignoreArticleId) { + const [existingArticle] = await articleRepository.findRows({ + where: eq("id", ignoreArticleId), + columns: ["id", "handle"], + limit: 1, + }); + + // If the article exists and its handle is already the slugified title, + // we can just return that handle (no need to append a number) + if (existingArticle && existingArticle.handle === baseHandle) { + return baseHandle; + } + } + + // Find all articles with the same base handle or handles that have numeric suffixes + const handlePattern = `${baseHandle}-%`; + let baseHandleWhereClause: any = eq( + "handle", + baseHandle + ); + let suffixWhereClause: any = like
("handle", handlePattern); + + let whereClause: any = or(baseHandleWhereClause, suffixWhereClause); + + if (ignoreArticleId) { + whereClause = and( + whereClause, + neq("id", ignoreArticleId) + ); + } + + // Get all existing handles that match our patterns + const existingArticles = await articleRepository.findRows({ + where: whereClause, + columns: ["handle"], }); - if (count) { - return `${slugify(title)}-${count + 1}`; + + // If no existing handles found, return the base handle + if (existingArticles.length === 0) { + return baseHandle; } - return slugify(title); + + // Check if the exact base handle exists + const exactBaseExists = existingArticles.some( + (article) => article.handle === baseHandle + ); + + // If the exact base handle doesn't exist, we can use it + if (!exactBaseExists) { + return baseHandle; + } + + // Find the highest numbered suffix + let highestNumber = 1; + const regex = new RegExp(`^${baseHandle}-(\\d+)$`); + + existingArticles.forEach((article) => { + const match = article.handle.match(regex); + if (match) { + const num = parseInt(match[1], 10); + if (num >= highestNumber) { + highestNumber = num + 1; + } + } + }); + + // Return with the next number in sequence + return `${baseHandle}-${highestNumber}`; } catch (error) { handleRepositoryException(error); + throw error; } }; @@ -116,7 +186,9 @@ export async function updateArticle( where: eq("id", input.article_id), data: { title: input.title, - handle: input.handle, + handle: input.handle + ? await getUniqueArticleHandle(input.handle, input.article_id) + : undefined, excerpt: input.excerpt, body: input.body, cover_image: input.cover_image, diff --git a/src/components/Editor/ArticleEditor.tsx b/src/components/Editor/ArticleEditor.tsx index 051efc6..088dc47 100644 --- a/src/components/Editor/ArticleEditor.tsx +++ b/src/components/Editor/ArticleEditor.tsx @@ -44,8 +44,14 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { const appConfig = useAppConfirm(); const titleRef = useRef(null!); const bodyRef = useRef(null); - const setDebouncedTitle = useDebouncedCallback(() => handleSaveTitle(), 1000); - const setDebouncedBody = useDebouncedCallback(() => handleSaveBody(), 1000); + const setDebouncedTitle = useDebouncedCallback( + (title: string) => handleDebouncedSaveTitle(title), + 1000 + ); + const setDebouncedBody = useDebouncedCallback( + (body: string) => handleDebouncedSaveBody(body), + 1000 + ); const [editorMode, selectEditorMode] = React.useState<"write" | "preview">( "write" @@ -91,39 +97,43 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { }, }); - const handleSaveTitle = () => { + const handleSaveArticleOnBlurTitle = (title: string) => { if (!uuid) { - if (editorForm.watch("title")) { + if (title) { articleCreateMutation.mutate({ - title: editorForm.watch("title") ?? "", + title: title ?? "", }); } } + }; + const handleDebouncedSaveTitle = (title: string) => { if (uuid) { - if (editorForm.watch("title")) { + if (title) { updateMyArticleMutation.mutate({ + title: title ?? "", article_id: uuid, - title: editorForm.watch("title") ?? "", }); } } }; - const handleSaveBody = () => { - // if (!uuid) { - // if (editorForm.watch("body")) { - // articleCreateMutation.mutate({ - // title: editorForm.watch("body") ?? "", - // }); - // } - // } - + const handleDebouncedSaveBody = (body: string) => { if (uuid) { - if (editorForm.watch("body")) { + if (body) { updateMyArticleMutation.mutate({ article_id: uuid, - body: editorForm.watch("body") ?? "", + handle: article?.handle ?? "untitled", + body, + }); + } + } else { + if (body) { + articleCreateMutation.mutate({ + title: editorForm.watch("title")?.length + ? (editorForm.watch("title") ?? "untitled") + : "untitled", + body, }); } } @@ -229,7 +239,7 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { value={editorForm.watch("title")} className="w-full text-2xl focus:outline-none bg-background resize-none" ref={titleRef} - onBlur={() => handleSaveTitle()} + onBlur={(e) => handleSaveArticleOnBlurTitle(e.target.value)} onChange={(e) => { editorForm.setValue("title", e.target.value); setDebouncedTitle(e.target.value); diff --git a/src/components/Editor/ArticleEditorDrawer.tsx b/src/components/Editor/ArticleEditorDrawer.tsx index 71391eb..f38e610 100644 --- a/src/components/Editor/ArticleEditorDrawer.tsx +++ b/src/components/Editor/ArticleEditorDrawer.tsx @@ -105,6 +105,8 @@ const ArticleEditorDrawer: React.FC = ({ article, open, onClose }) => { onSubmit={form.handleSubmit(handleOnSubmit)} className="flex flex-col gap-2" > + {JSON.stringify(form.formState.errors)} + Date: Fri, 11 Apr 2025 15:36:44 +0600 Subject: [PATCH 30/32] Refactor ArticleEditor component to improve state management and debounced save functionality for title and body. Update type definitions for props and enhance editor mode toggling. Adjust textarea references for better handling of null values. --- src/components/Editor/ArticleEditor.tsx | 267 +++++++++++++----------- src/hooks/use-auto-resize-textarea.ts | 2 +- 2 files changed, 142 insertions(+), 127 deletions(-) diff --git a/src/components/Editor/ArticleEditor.tsx b/src/components/Editor/ArticleEditor.tsx index 088dc47..6135aa4 100644 --- a/src/components/Editor/ArticleEditor.tsx +++ b/src/components/Editor/ArticleEditor.tsx @@ -13,7 +13,7 @@ import { HeadingIcon, ImageIcon, } from "@radix-ui/react-icons"; -import React, { useRef } from "react"; +import React, { useRef, useState, useCallback } from "react"; import { ArticleRepositoryInput } from "@/backend/services/inputs/article.input"; import { useAutosizeTextArea } from "@/hooks/use-auto-resize-textarea"; @@ -32,30 +32,20 @@ import ArticleEditorDrawer from "./ArticleEditorDrawer"; import EditorCommandButton from "./EditorCommandButton"; import { useMarkdownEditor } from "./useMarkdownEditor"; -interface Prop { +interface ArticleEditorProps { uuid?: string; article?: Article; } -const ArticleEditor: React.FC = ({ article, uuid }) => { +const ArticleEditor: React.FC = ({ article, uuid }) => { const { _t, lang } = useTranslation(); const router = useRouter(); const [isOpenSettingDrawer, toggleSettingDrawer] = useToggle(); const appConfig = useAppConfirm(); - const titleRef = useRef(null!); + const titleRef = useRef(null); const bodyRef = useRef(null); - const setDebouncedTitle = useDebouncedCallback( - (title: string) => handleDebouncedSaveTitle(title), - 1000 - ); - const setDebouncedBody = useDebouncedCallback( - (body: string) => handleDebouncedSaveBody(body), - 1000 - ); + const [editorMode, setEditorMode] = useState<"write" | "preview">("write"); - const [editorMode, selectEditorMode] = React.useState<"write" | "preview">( - "write" - ); const editorForm = useForm({ defaultValues: { title: article?.title || "", @@ -64,107 +54,169 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { resolver: zodResolver(ArticleRepositoryInput.updateArticleInput), }); - useAutosizeTextArea(titleRef, editorForm.watch("title") ?? ""); + const watchedTitle = editorForm.watch("title"); + const watchedBody = editorForm.watch("body"); - const editor = useMarkdownEditor({ - ref: bodyRef, - onChange: handleBodyContentChange, - }); + useAutosizeTextArea(titleRef, watchedTitle ?? ""); const updateMyArticleMutation = useMutation({ mutationFn: ( input: z.infer - ) => { - return articleActions.updateMyArticle(input); - }, - onSuccess: () => { - router.refresh(); - }, - onError(err) { - alert(err.message); - }, + ) => articleActions.updateMyArticle(input), + onSuccess: () => router.refresh(), + onError: (err) => alert(err.message), }); const articleCreateMutation = useMutation({ mutationFn: ( input: z.infer ) => articleActions.createMyArticle(input), - onSuccess: (res) => { - router.push(`/dashboard/articles/${res?.id}`); - }, - onError(err) { - alert(err.message); - }, + onSuccess: (res) => router.push(`/dashboard/articles/${res?.id}`), + onError: (err) => alert(err.message), }); - const handleSaveArticleOnBlurTitle = (title: string) => { - if (!uuid) { - if (title) { - articleCreateMutation.mutate({ - title: title ?? "", - }); - } - } - }; - - const handleDebouncedSaveTitle = (title: string) => { - if (uuid) { - if (title) { + const handleDebouncedSaveTitle = useCallback( + (title: string) => { + if (uuid && title) { updateMyArticleMutation.mutate({ - title: title ?? "", + title, article_id: uuid, }); } - } - }; + }, + [uuid, updateMyArticleMutation] + ); + + const handleDebouncedSaveBody = useCallback( + (body: string) => { + if (!body) return; - const handleDebouncedSaveBody = (body: string) => { - if (uuid) { - if (body) { + if (uuid) { updateMyArticleMutation.mutate({ article_id: uuid, handle: article?.handle ?? "untitled", body, }); - } - } else { - if (body) { + } else { articleCreateMutation.mutate({ - title: editorForm.watch("title")?.length - ? (editorForm.watch("title") ?? "untitled") + title: watchedTitle?.length + ? (watchedTitle ?? "untitled") : "untitled", body, }); } - } - }; + }, + [ + uuid, + article?.handle, + watchedTitle, + updateMyArticleMutation, + articleCreateMutation, + ] + ); + + const setDebouncedTitle = useDebouncedCallback( + handleDebouncedSaveTitle, + 1000 + ); + const setDebouncedBody = useDebouncedCallback(handleDebouncedSaveBody, 1000); + + const handleSaveArticleOnBlurTitle = useCallback( + (title: string) => { + if (!uuid && title) { + articleCreateMutation.mutate({ + title, + }); + } + }, + [uuid, articleCreateMutation] + ); - function handleBodyContentChange( - e: React.ChangeEvent | string - ) { - const value = typeof e === "string" ? e : e.target.value; - editorForm.setValue("body", value); - setDebouncedBody(value); - } + const handleBodyContentChange = useCallback( + (e: React.ChangeEvent | string) => { + const value = typeof e === "string" ? e : e.target.value; + editorForm.setValue("body", value); + setDebouncedBody(value); + }, + [editorForm, setDebouncedBody] + ); + + const editor = useMarkdownEditor({ + ref: bodyRef, + onChange: handleBodyContentChange, + }); + + const toggleEditorMode = useCallback( + () => setEditorMode((mode) => (mode === "write" ? "preview" : "write")), + [] + ); + + const handlePublishToggle = useCallback(() => { + appConfig.show({ + title: _t("Are you sure?"), + labels: { + confirm: _t("Yes"), + cancel: _t("No"), + }, + onConfirm: () => { + if (uuid) { + updateMyArticleMutation.mutate({ + article_id: uuid, + is_published: !article?.is_published, + }); + } + }, + }); + }, [appConfig, _t, uuid, article?.is_published, updateMyArticleMutation]); + + const handleTitleChange = useCallback( + (e: React.ChangeEvent) => { + const value = e.target.value; + editorForm.setValue("title", value); + setDebouncedTitle(value); + }, + [editorForm, setDebouncedTitle] + ); + + const renderEditorToolbar = () => ( +
+ editor?.executeCommand("heading")} + Icon={} + /> + editor?.executeCommand("bold")} + Icon={} + /> + editor?.executeCommand("italic")} + Icon={} + /> + editor?.executeCommand("image")} + Icon={} + /> +
+ ); return ( <>
- + {updateMyArticleMutation.isPending ? (

{_t("Saving")}...

) : ( -

- {article?.updated_at && ( + article?.updated_at && ( +

- ({_t("Saved")} {formattedTime(article?.updated_at, lang)}) + ({_t("Saved")} {formattedTime(article.updated_at, lang)}) - )} -

+

+ ) )}
@@ -187,30 +239,14 @@ const ArticleEditor: React.FC = ({ article, uuid }) => { {uuid && (
-
)}
- {/* Editor */}
+ /> ) : (
- {markdocParser(editorForm.watch("body") ?? "")} + {markdocParser(watchedBody ?? "")}
)}
- {uuid && ( + {uuid && article && ( { + // Implementation needed }} /> )} diff --git a/src/hooks/use-auto-resize-textarea.ts b/src/hooks/use-auto-resize-textarea.ts index 7463c82..e581622 100644 --- a/src/hooks/use-auto-resize-textarea.ts +++ b/src/hooks/use-auto-resize-textarea.ts @@ -10,7 +10,7 @@ import { useEffect } from "react"; * return