From 14c72367c4455015b1c35a54cb841ef168e3e835 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:25:20 +0600 Subject: [PATCH 1/8] docs: update CLAUDE.md to reflect authentication and Gist enhancements - Changed authentication method to WorkOS as primary, with GitHub OAuth as a legacy fallback. - Added new route for Gist browsing, creation, and viewing. - Documented Gists as a key entity with associated tables in the database. - Updated available repositories list to include 'gist' and 'gistFile'. --- CLAUDE.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index dfafdc1..18c88e6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -33,7 +33,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - **Styling**: Tailwind CSS 4, shadcn/ui components - **Backend**: Next.js Server Actions, Drizzle ORM (migrations only) - **Database**: PostgreSQL -- **Authentication**: GitHub OAuth +- **Authentication**: WorkOS (primary), GitHub OAuth (legacy fallback) - **Search**: MeilSearch - **File Storage**: Cloudinary / Cloudflare R2 - **State Management**: Jotai, TanStack Query, React Hook Form with Zod validation @@ -52,6 +52,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co - Route groups using Next.js App Router: - `(home)` - Main homepage and article feed - `(dashboard-editor)` - Protected dashboard routes + - `gists` - Gist browsing, creation, and viewing - `[username]` - User profile pages - `[username]/[articleHandle]` - Individual article pages - API routes in `/api/` for OAuth and development @@ -79,6 +80,7 @@ Key entities and their relationships: - **Tags** - Article categorization - **Bookmarks** - User content saving - **Reactions** - Emoji-based reactions (LOVE, FIRE, WOW, etc.) +- **Gists** - Code snippets with multiple files (`gists` + `gist_files` tables) - **User Sessions** - Session management - **User Socials** - OAuth provider connections @@ -208,7 +210,7 @@ persistenceRepository.article.paginate({ where, orderBy, limit, page }) persistenceRepository.article.find({ where, columns, joins }) ``` -Available repositories: `user`, `userSocial`, `userSession`, `article`, `bookmark`, `comment`, `reaction`, `articleTagPivot`, `tags`, `series`, `seriesItems`, `kv`. +Available repositories: `user`, `userSocial`, `userSession`, `article`, `bookmark`, `comment`, `reaction`, `articleTagPivot`, `tags`, `series`, `seriesItems`, `kv`, `gist`, `gistFile`. For complex multi-join queries, raw SQL is executed directly via `pgClient.executeSQL()`. From 619949707abe9c322d29f7ce5fe1494b7404c821 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:27:25 +0600 Subject: [PATCH 2/8] fix: improve error handling in bookmark and reaction services - Updated error handling in toggleResourceBookmark and myBookmarks functions to return exceptions instead of just logging them. - Enhanced bookmarkStatus function to return a default value when an error occurs. - Refactored reaction actions to utilize a constant for reaction types and added a helper function for empty reaction statuses. --- src/backend/services/bookmark.action.ts | 5 +-- src/backend/services/reaction.actions.ts | 41 +++++++++++++++++------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/src/backend/services/bookmark.action.ts b/src/backend/services/bookmark.action.ts index 0f9a326..d00432e 100644 --- a/src/backend/services/bookmark.action.ts +++ b/src/backend/services/bookmark.action.ts @@ -55,7 +55,7 @@ export async function toggleResourceBookmark( ]); return { bookmarked: true }; } catch (error) { - handleActionException(error); + return handleActionException(error); } } @@ -126,7 +126,7 @@ export async function myBookmarks( }, }; } catch (error) { - handleActionException(error); + return handleActionException(error); } } @@ -160,5 +160,6 @@ export async function bookmarkStatus( return { bookmarked: Boolean(existingBookmark) }; } catch (error) { handleActionException(error); + return { bookmarked: false }; } } diff --git a/src/backend/services/reaction.actions.ts b/src/backend/services/reaction.actions.ts index 2c83f9e..417a3fb 100644 --- a/src/backend/services/reaction.actions.ts +++ b/src/backend/services/reaction.actions.ts @@ -11,6 +11,24 @@ import { ReactionStatus } from "../models/domain-models"; const sql = String.raw; +const REACTION_TYPES = [ + "LOVE", + "UNICORN", + "WOW", + "FIRE", + "CRY", + "HAHA", +] as const; + +function emptyReactionStatuses(): ReactionStatus[] { + return REACTION_TYPES.map((reaction_type) => ({ + reaction_type, + count: 0, + is_reacted: false, + reactor_user_ids: [], + })); +} + export async function toogleReaction( _input: z.infer ) { @@ -66,7 +84,7 @@ export async function toogleReaction( is_reacted: true, }; } catch (error) { - handleActionException(error); + return handleActionException(error); } } @@ -117,18 +135,17 @@ export async function getResourceReactions( } // Return all types, filling missing ones with count: 0 - return ["LOVE", "UNICORN", "WOW", "FIRE", "CRY", "HAHA"].map( - (reaction_type) => { - const entry = reactionMap.get(reaction_type); - return { - reaction_type, - count: entry?.count ?? 0, - is_reacted: entry?.is_reacted ?? false, - reactor_user_ids: entry?.reactor_user_ids ?? [], - }; - } - ); + return REACTION_TYPES.map((reaction_type) => { + const entry = reactionMap.get(reaction_type); + return { + reaction_type, + count: entry?.count ?? 0, + is_reacted: entry?.is_reacted ?? false, + reactor_user_ids: entry?.reactor_user_ids ?? [], + }; + }); } catch (error) { handleActionException(error); + return emptyReactionStatuses(); } } From 6368aaaaebf93ef030e4c471dacac25503777968 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:29:36 +0600 Subject: [PATCH 3/8] refactor: enhance bookmarks handling and improve state management - Introduced a type guard function `isBookmarksSuccess` to validate bookmark data structure. - Updated pagination logic to ensure safe access to bookmark properties. - Refactored item presence check to utilize the new type guard for better reliability. - Improved state update in `ResourceBookmarkable` to ensure accurate bookmark status after mutation. --- src/app/dashboard/bookmarks/page.tsx | 20 +++++++++++++------ .../render-props/ResourceBookmarkable.tsx | 5 +++-- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/src/app/dashboard/bookmarks/page.tsx b/src/app/dashboard/bookmarks/page.tsx index b241842..765341d 100644 --- a/src/app/dashboard/bookmarks/page.tsx +++ b/src/app/dashboard/bookmarks/page.tsx @@ -27,6 +27,12 @@ interface BookmarkData { meta: BookmarkMeta; } +function isBookmarksSuccess( + page: Awaited> | undefined +): page is BookmarkData { + return Boolean(page && "meta" in page && "nodes" in page); +} + const BookmarksPage = () => { const { _t } = useTranslation(); const feedInfiniteQuery = useInfiniteQuery({ @@ -35,15 +41,16 @@ const BookmarksPage = () => { myBookmarks({ limit: 10, page: pageParam, offset: 0 }), initialPageParam: 1, getNextPageParam: (lastPage) => { - const _page = lastPage?.meta?.currentPage ?? 1; - const _totalPages = lastPage?.meta?.totalPages ?? 1; + if (!isBookmarksSuccess(lastPage)) return null; + const _page = lastPage.meta.currentPage; + const _totalPages = lastPage.meta.totalPages; return _page + 1 <= _totalPages ? _page + 1 : null; }, }); const hasItems = useMemo(() => { - const length = feedInfiniteQuery.data?.pages.flat()[0]?.nodes.length ?? 0; - return length > 0; + const firstOk = feedInfiniteQuery.data?.pages.find(isBookmarksSuccess); + return (firstOk?.nodes.length ?? 0) > 0; }, [feedInfiniteQuery]); const appConfirm = useAppConfirm(); @@ -65,8 +72,9 @@ const BookmarksPage = () => {
))} - {feedInfiniteQuery.data?.pages.map((page) => { - return page?.nodes.map((bookmark) => ( + {feedInfiniteQuery.data?.pages.flatMap((page) => { + if (!isBookmarksSuccess(page)) return []; + return page.nodes.map((bookmark) => (
= ({ mutationFn: () => bookmarkAction.toggleResourceBookmark({ resource_id, resource_type }), - // Ensure state is accurate after success onSuccess: (data) => { - setBookmarked(data?.bookmarked ?? false); + if (data && "bookmarked" in data) { + setBookmarked(data.bookmarked); + } }, }); From 062cac562c91a61c164a73eea7df170dc4857a72 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:30:40 +0600 Subject: [PATCH 4/8] refactor: streamline Gist retrieval logic and enhance error handling - Simplified the visibility check by consolidating conditions for public and private gists. - Improved error handling to throw exceptions for missing gists and unauthorized access. - Enhanced code readability by reducing redundancy in the gist retrieval process. --- src/backend/services/gist.actions.ts | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/backend/services/gist.actions.ts b/src/backend/services/gist.actions.ts index 2498cf2..c701610 100644 --- a/src/backend/services/gist.actions.ts +++ b/src/backend/services/gist.actions.ts @@ -125,20 +125,16 @@ export async function getGist( ], }); - // check visibility - if (!gistFindResponse[0].is_public) { - if (gistFindResponse[0].owner_id !== sessionUserId) { - throw new ActionException("Not authorized to view this gist"); - } + const row = gistFindResponse[0]; + if (!row) { + throw new ActionException("Gist not found"); } - if (gistFindResponse[0]) { - gist = gistFindResponse[0]; + if (!row.is_public && row.owner_id !== sessionUserId) { + throw new ActionException("Not authorized to view this gist"); } - if (!gist) { - throw new ActionException("Gist not found"); - } + gist = row; const gistFiles = await persistenceRepository.gistFile.find({ where: eq("gist_id", gist.id), From f9c31ba32033fbff1e19750e4e0e1ba60de2b0eb Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:51:48 +0600 Subject: [PATCH 5/8] feat: add image export functionality to GistViewer - Introduced a button to export Gist files as images using a Carbon-style format. - Added a dialog component for handling image export, allowing users to preview and save Gist content as images. - Integrated state management for the image export feature within the GistViewer component. --- package.json | 1 + src/components/Gist/GistCodeImageDialog.tsx | 412 ++++++++++++++++++++ src/components/Gist/GistViewer.tsx | 25 ++ 3 files changed, 438 insertions(+) create mode 100644 src/components/Gist/GistCodeImageDialog.tsx diff --git a/package.json b/package.json index 661b9e0..73e4aae 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,7 @@ "lottie-react": "^2.4.1", "lucide-react": "^0.484.0", "meilisearch": "^0.51.0", + "modern-screenshot": "^4.6.8", "next": "^16.2.1", "next-themes": "^0.4.6", "pg": "^8.14.1", diff --git a/src/components/Gist/GistCodeImageDialog.tsx b/src/components/Gist/GistCodeImageDialog.tsx new file mode 100644 index 0000000..4f8695f --- /dev/null +++ b/src/components/Gist/GistCodeImageDialog.tsx @@ -0,0 +1,412 @@ +"use client"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Label } from "@/components/ui/label"; +import { cn } from "@/lib/utils"; +import { + dracula, + nightOwl, + nord, + okaidia, + oneDark, + shadesOfPurple, + vscDarkPlus, +} from "react-syntax-highlighter/dist/esm/styles/prism"; +import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; +import { useCallback, useMemo, useRef, useState } from "react"; +import { toast } from "sonner"; +import { Download, Loader2 } from "lucide-react"; +import { domToBlob } from "modern-screenshot"; + +type PrismTheme = typeof vscDarkPlus; + +const THEMES: { id: string; label: string; style: PrismTheme }[] = [ + { id: "vsc", label: "VS Code Dark+", style: vscDarkPlus }, + { id: "night", label: "Night Owl", style: nightOwl }, + { id: "dracula", label: "Dracula", style: dracula }, + { id: "one", label: "One Dark", style: oneDark }, + { id: "okaidia", label: "Okaidia", style: okaidia }, + { id: "purple", label: "Shades of Purple", style: shadesOfPurple }, + { id: "nord", label: "Nord", style: nord }, +]; + +const BACKGROUNDS: { id: string; label: string; css: string }[] = [ + { + id: "aurora", + label: "Aurora", + css: "linear-gradient(145deg, #0f0c29 0%, #302b63 45%, #24243e 100%)", + }, + { + id: "ember", + label: "Ember", + css: "linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%)", + }, + { + id: "forest", + label: "Forest", + css: "linear-gradient(135deg, #134e5e 0%, #71b280 100%)", + }, + { + id: "sunset", + label: "Sunset", + css: "linear-gradient(135deg, #2d1b4e 0%, #8b2500 50%, #ff6b35 100%)", + }, + { id: "void", label: "Void", css: "#0d1117" }, + { id: "slate", label: "Slate", css: "#1e293b" }, +]; + +function guessLanguage(filename: string, fileLanguage?: string | null): string { + const l = fileLanguage?.trim().toLowerCase(); + if (l) { + if (l === "md" || l === "markdown") return "markdown"; + return l; + } + const ext = filename.split(".").pop()?.toLowerCase() ?? ""; + const map: Record = { + ts: "typescript", + tsx: "tsx", + js: "javascript", + jsx: "jsx", + mjs: "javascript", + cjs: "javascript", + py: "python", + rb: "ruby", + go: "go", + rs: "rust", + md: "markdown", + json: "json", + css: "css", + scss: "scss", + html: "markup", + xml: "markup", + svg: "markup", + yml: "yaml", + yaml: "yaml", + sh: "bash", + bash: "bash", + zsh: "bash", + sql: "sql", + graphql: "graphql", + vue: "markup", + java: "java", + kt: "kotlin", + swift: "swift", + php: "php", + }; + return map[ext] ?? "text"; +} + +interface GistCodeImageDialogProps { + open: boolean; + onOpenChange: (open: boolean) => void; + filename: string; + content: string; + language?: string | null; +} + +export default function GistCodeImageDialog({ + open, + onOpenChange, + filename, + content, + language, +}: GistCodeImageDialogProps) { + const captureRef = useRef(null); + const [themeId, setThemeId] = useState(THEMES[0].id); + const [bgId, setBgId] = useState(BACKGROUNDS[0].id); + const [padding, setPadding] = useState(40); + const [frameWidth, setFrameWidth] = useState(920); + const [lineNumbers, setLineNumbers] = useState(false); + const [exporting, setExporting] = useState(false); + + const theme = useMemo( + () => THEMES.find((t) => t.id === themeId) ?? THEMES[0], + [themeId], + ); + const background = useMemo( + () => BACKGROUNDS.find((b) => b.id === bgId) ?? BACKGROUNDS[0], + [bgId], + ); + const lang = useMemo( + () => guessLanguage(filename, language), + [filename, language], + ); + + const handleExport = useCallback(async () => { + const el = captureRef.current; + if (!el) { + toast.error("Nothing to capture"); + return; + } + setExporting(true); + let clone: HTMLElement | null = null; + try { + await document.fonts.ready; + await new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + }); + + /** + * Radix Dialog centers with transform; rasterizing that subtree skews the SVG/canvas + * (black band, clipped right side). Clone to document.body with transform:none and + * explicit size so the export matches the preview without ancestor transforms. + */ + const naturalW = Math.ceil(Math.max(el.scrollWidth, el.offsetWidth)); + clone = el.cloneNode(true) as HTMLElement; + clone.style.position = "fixed"; + clone.style.left = "0"; + clone.style.top = "0"; + clone.style.width = `${naturalW}px`; + clone.style.maxWidth = "none"; + clone.style.height = "auto"; + clone.style.minHeight = "0"; + clone.style.margin = "0"; + clone.style.transform = "none"; + clone.style.boxSizing = "border-box"; + clone.style.overflow = "visible"; + /* Below the dialog (z-50) so export doesn’t flash on top of the UI; rasterizer still reads the DOM. */ + clone.style.zIndex = "40"; + clone.style.pointerEvents = "none"; + document.body.appendChild(clone); + + await new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + }); + + const w2 = Math.ceil(Math.max(clone.offsetWidth, clone.scrollWidth)); + const h2 = Math.ceil(clone.scrollHeight); + const scale = Math.min(4, Math.max(3, window.devicePixelRatio || 2)); + const blob = await domToBlob(clone, { + scale, + width: w2, + height: h2, + backgroundColor: null, + }); + + if (!blob || blob.size < 32) { + toast.error("Could not create image"); + return; + } + + const safeName = + filename.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 80) || "code"; + const url = URL.createObjectURL(blob); + const link = document.createElement("a"); + link.download = `${safeName}.png`; + link.href = url; + link.click(); + URL.revokeObjectURL(url); + toast.success("Image downloaded"); + } catch (e) { + console.error(e); + toast.error("Could not create image"); + } finally { + clone?.remove(); + setExporting(false); + } + }, [filename]); + + return ( + + + + Code image + + +
+
+
+ + +
+
+ + +
+
+ +
+
+ + setPadding(Number(e.target.value))} + className="w-full accent-primary" + /> +
+
+ + setFrameWidth(Number(e.target.value))} + className="w-full accent-primary" + /> +
+
+ + +
+ +
+
+
+
+ + + + + {filename} + +
+ + {content} + +
+
+
+ + + + + +
+
+ ); +} diff --git a/src/components/Gist/GistViewer.tsx b/src/components/Gist/GistViewer.tsx index 3da8e99..5d04740 100644 --- a/src/components/Gist/GistViewer.tsx +++ b/src/components/Gist/GistViewer.tsx @@ -18,6 +18,8 @@ import { Link2Icon, FileIcon, } from "@radix-ui/react-icons"; +import { ImageIcon } from "lucide-react"; +import GistCodeImageDialog from "@/components/Gist/GistCodeImageDialog"; import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useRouter } from "next/navigation"; import { useState } from "react"; @@ -86,6 +88,9 @@ export default function GistViewer({ const router = useRouter(); const queryClient = useQueryClient(); const appConfirm = useAppConfirm(); + const [imageExportFile, setImageExportFile] = useState( + null + ); const renderFileContent = (file: GistFile) => { const ext = file.filename ? file.filename.split(".").pop() : undefined; @@ -257,6 +262,16 @@ export default function GistViewer({
+ )} + + { + if (!open) setImageExportFile(null); + }} + filename={imageExportFile?.filename ?? ""} + content={imageExportFile?.content ?? ""} + language={imageExportFile?.language} + />
); } From 01831a87c42970e79f873c4d0b699410cd95e770 Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:54:54 +0600 Subject: [PATCH 6/8] feat: add clipboard copy functionality to GistCodeImageDialog - Introduced a new button to copy the exported image directly to the clipboard. - Refactored the image export logic to improve clarity and error handling. - Enhanced user feedback during image export and copy operations with loading indicators. --- src/components/Gist/GistCodeImageDialog.tsx | 96 +++++++++++++++------ 1 file changed, 72 insertions(+), 24 deletions(-) diff --git a/src/components/Gist/GistCodeImageDialog.tsx b/src/components/Gist/GistCodeImageDialog.tsx index 4f8695f..d73bdf1 100644 --- a/src/components/Gist/GistCodeImageDialog.tsx +++ b/src/components/Gist/GistCodeImageDialog.tsx @@ -23,7 +23,7 @@ import { import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { useCallback, useMemo, useRef, useState } from "react"; import { toast } from "sonner"; -import { Download, Loader2 } from "lucide-react"; +import { ClipboardCopy, Download, Loader2 } from "lucide-react"; import { domToBlob } from "modern-screenshot"; type PrismTheme = typeof vscDarkPlus; @@ -140,25 +140,21 @@ export default function GistCodeImageDialog({ [filename, language], ); - const handleExport = useCallback(async () => { + /** + * Radix Dialog centers with transform; rasterizing that subtree skews the SVG/canvas. + * Clone to document.body with transform:none so export matches the preview. + */ + const createCaptureBlob = useCallback(async (): Promise => { const el = captureRef.current; - if (!el) { - toast.error("Nothing to capture"); - return; - } - setExporting(true); + if (!el) return null; + + await document.fonts.ready; + await new Promise((resolve) => { + requestAnimationFrame(() => requestAnimationFrame(() => resolve())); + }); + let clone: HTMLElement | null = null; try { - await document.fonts.ready; - await new Promise((resolve) => { - requestAnimationFrame(() => requestAnimationFrame(() => resolve())); - }); - - /** - * Radix Dialog centers with transform; rasterizing that subtree skews the SVG/canvas - * (black band, clipped right side). Clone to document.body with transform:none and - * explicit size so the export matches the preview without ancestor transforms. - */ const naturalW = Math.ceil(Math.max(el.scrollWidth, el.offsetWidth)); clone = el.cloneNode(true) as HTMLElement; clone.style.position = "fixed"; @@ -172,7 +168,6 @@ export default function GistCodeImageDialog({ clone.style.transform = "none"; clone.style.boxSizing = "border-box"; clone.style.overflow = "visible"; - /* Below the dialog (z-50) so export doesn’t flash on top of the UI; rasterizer still reads the DOM. */ clone.style.zIndex = "40"; clone.style.pointerEvents = "none"; document.body.appendChild(clone); @@ -191,7 +186,18 @@ export default function GistCodeImageDialog({ backgroundColor: null, }); - if (!blob || blob.size < 32) { + if (!blob || blob.size < 32) return null; + return blob; + } finally { + clone?.remove(); + } + }, []); + + const handleExport = useCallback(async () => { + setExporting(true); + try { + const blob = await createCaptureBlob(); + if (!blob) { toast.error("Could not create image"); return; } @@ -209,10 +215,34 @@ export default function GistCodeImageDialog({ console.error(e); toast.error("Could not create image"); } finally { - clone?.remove(); setExporting(false); } - }, [filename]); + }, [createCaptureBlob, filename]); + + const handleCopyImage = useCallback(async () => { + if (!navigator.clipboard?.write || typeof ClipboardItem === "undefined") { + toast.error("Copying images is not supported in this browser"); + return; + } + + setExporting(true); + try { + const blob = await createCaptureBlob(); + if (!blob) { + toast.error("Could not create image"); + return; + } + + const type = blob.type && blob.type.startsWith("image/") ? blob.type : "image/png"; + await navigator.clipboard.write([new ClipboardItem({ [type]: blob })]); + toast.success("Image copied to clipboard"); + } catch (e) { + console.error(e); + toast.error("Could not copy image"); + } finally { + setExporting(false); + } + }, [createCaptureBlob]); return ( @@ -301,7 +331,7 @@ export default function GistCodeImageDialog({ -
+
- + +
+
+ + techdiary.dev + +
From 09564c52e6d5d5c122124e04762a444f4b2d2dae Mon Sep 17 00:00:00 2001 From: kingrayhan Date: Mon, 30 Mar 2026 00:59:23 +0600 Subject: [PATCH 8/8] chore: release version 1.3.0 with new features and improvements - Updated CHANGELOG.md to document new features, bug fixes, and other changes for version 1.3.0. - Bumped version number in package.json to 1.3.0. - Added clipboard copy and image export functionalities to Gist components. - Improved error handling in bookmark and reaction services. - Refactored Gist retrieval and bookmarks handling for better performance and reliability. --- CHANGELOG.md | 17 +++++++++++++++++ package.json | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f725324..ba2900a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,23 @@ This project adheres to [Semantic Versioning](https://semver.org/). --- +## v1.3.0 — 2026-03-30 + +### ✨ Features +- feat: add footer with attribution to techdiary.dev in GistCodeImageDialog (b7c15b9) +- feat: add clipboard copy functionality to GistCodeImageDialog (01831a8) +- feat: add image export functionality to GistViewer (f9c31ba) + +### 🐛 Bug Fixes +- fix: improve error handling in bookmark and reaction services (6199497) + +### 🔧 Other Changes +- refactor: streamline Gist retrieval logic and enhance error handling (062cac5) +- refactor: enhance bookmarks handling and improve state management (6368aaa) +- docs: update CLAUDE.md to reflect authentication and Gist enhancements (14c7236) + +--- + ## v1.2.0 — 2026-03-30 ### ✨ Features diff --git a/package.json b/package.json index 73e4aae..b4247e5 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "techdiary.dev-next", - "version": "1.2.0", + "version": "1.3.0", "private": true, "scripts": { "dev": "next dev --turbo",