Skip to content
Merged
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 4 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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()`.

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "techdiary.dev-next",
"version": "1.2.0",
"version": "1.3.0",
"private": true,
"scripts": {
"dev": "next dev --turbo",
Expand Down Expand Up @@ -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",
Expand Down
20 changes: 14 additions & 6 deletions src/app/dashboard/bookmarks/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ interface BookmarkData {
meta: BookmarkMeta;
}

function isBookmarksSuccess(
page: Awaited<ReturnType<typeof myBookmarks>> | undefined
): page is BookmarkData {
return Boolean(page && "meta" in page && "nodes" in page);
}

const BookmarksPage = () => {
const { _t } = useTranslation();
const feedInfiniteQuery = useInfiniteQuery({
Expand All @@ -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();
Expand All @@ -65,8 +72,9 @@ const BookmarksPage = () => {
<article key={i} className=" bg-muted h-20 animate-pulse" />
))}

{feedInfiniteQuery.data?.pages.map((page) => {
return page?.nodes.map((bookmark) => (
{feedInfiniteQuery.data?.pages.flatMap((page) => {
if (!isBookmarksSuccess(page)) return [];
return page.nodes.map((bookmark) => (
<article
key={bookmark.id}
className="flex justify-between flex-col md:flex-row py-3 space-y-2"
Expand Down
5 changes: 3 additions & 2 deletions src/backend/services/bookmark.action.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ export async function toggleResourceBookmark(
]);
return { bookmarked: true };
} catch (error) {
handleActionException(error);
return handleActionException(error);
}
}

Expand Down Expand Up @@ -126,7 +126,7 @@ export async function myBookmarks(
},
};
} catch (error) {
handleActionException(error);
return handleActionException(error);
}
}

Expand Down Expand Up @@ -160,5 +160,6 @@ export async function bookmarkStatus(
return { bookmarked: Boolean(existingBookmark) };
} catch (error) {
handleActionException(error);
return { bookmarked: false };
}
}
16 changes: 6 additions & 10 deletions src/backend/services/gist.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
41 changes: 29 additions & 12 deletions src/backend/services/reaction.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<typeof ReactionActionInput.toggleReactionInput>
) {
Expand Down Expand Up @@ -66,7 +84,7 @@ export async function toogleReaction(
is_reacted: true,
};
} catch (error) {
handleActionException(error);
return handleActionException(error);
}
}

Expand Down Expand Up @@ -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();
}
}
Loading