Skip to content

Commit 62c0159

Browse files
authored
Merge pull request #87 from techdiary-dev/kingrayhan/gist
Kingrayhan/gist
2 parents c40c0cf + 09564c5 commit 62c0159

10 files changed

Lines changed: 587 additions & 35 deletions

File tree

CHANGELOG.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,23 @@ This project adheres to [Semantic Versioning](https://semver.org/).
66

77
---
88

9+
## v1.3.0 — 2026-03-30
10+
11+
### ✨ Features
12+
- feat: add footer with attribution to techdiary.dev in GistCodeImageDialog (b7c15b9)
13+
- feat: add clipboard copy functionality to GistCodeImageDialog (01831a8)
14+
- feat: add image export functionality to GistViewer (f9c31ba)
15+
16+
### 🐛 Bug Fixes
17+
- fix: improve error handling in bookmark and reaction services (6199497)
18+
19+
### 🔧 Other Changes
20+
- refactor: streamline Gist retrieval logic and enhance error handling (062cac5)
21+
- refactor: enhance bookmarks handling and improve state management (6368aaa)
22+
- docs: update CLAUDE.md to reflect authentication and Gist enhancements (14c7236)
23+
24+
---
25+
926
## v1.2.0 — 2026-03-30
1027

1128
### ✨ Features

CLAUDE.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
3333
- **Styling**: Tailwind CSS 4, shadcn/ui components
3434
- **Backend**: Next.js Server Actions, Drizzle ORM (migrations only)
3535
- **Database**: PostgreSQL
36-
- **Authentication**: GitHub OAuth
36+
- **Authentication**: WorkOS (primary), GitHub OAuth (legacy fallback)
3737
- **Search**: MeilSearch
3838
- **File Storage**: Cloudinary / Cloudflare R2
3939
- **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
5252
- Route groups using Next.js App Router:
5353
- `(home)` - Main homepage and article feed
5454
- `(dashboard-editor)` - Protected dashboard routes
55+
- `gists` - Gist browsing, creation, and viewing
5556
- `[username]` - User profile pages
5657
- `[username]/[articleHandle]` - Individual article pages
5758
- API routes in `/api/` for OAuth and development
@@ -79,6 +80,7 @@ Key entities and their relationships:
7980
- **Tags** - Article categorization
8081
- **Bookmarks** - User content saving
8182
- **Reactions** - Emoji-based reactions (LOVE, FIRE, WOW, etc.)
83+
- **Gists** - Code snippets with multiple files (`gists` + `gist_files` tables)
8284
- **User Sessions** - Session management
8385
- **User Socials** - OAuth provider connections
8486

@@ -208,7 +210,7 @@ persistenceRepository.article.paginate({ where, orderBy, limit, page })
208210
persistenceRepository.article.find({ where, columns, joins })
209211
```
210212

211-
Available repositories: `user`, `userSocial`, `userSession`, `article`, `bookmark`, `comment`, `reaction`, `articleTagPivot`, `tags`, `series`, `seriesItems`, `kv`.
213+
Available repositories: `user`, `userSocial`, `userSession`, `article`, `bookmark`, `comment`, `reaction`, `articleTagPivot`, `tags`, `series`, `seriesItems`, `kv`, `gist`, `gistFile`.
212214

213215
For complex multi-join queries, raw SQL is executed directly via `pgClient.executeSQL()`.
214216

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "techdiary.dev-next",
3-
"version": "1.2.0",
3+
"version": "1.3.0",
44
"private": true,
55
"scripts": {
66
"dev": "next dev --turbo",
@@ -56,6 +56,7 @@
5656
"lottie-react": "^2.4.1",
5757
"lucide-react": "^0.484.0",
5858
"meilisearch": "^0.51.0",
59+
"modern-screenshot": "^4.6.8",
5960
"next": "^16.2.1",
6061
"next-themes": "^0.4.6",
6162
"pg": "^8.14.1",

src/app/dashboard/bookmarks/page.tsx

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ interface BookmarkData {
2727
meta: BookmarkMeta;
2828
}
2929

30+
function isBookmarksSuccess(
31+
page: Awaited<ReturnType<typeof myBookmarks>> | undefined
32+
): page is BookmarkData {
33+
return Boolean(page && "meta" in page && "nodes" in page);
34+
}
35+
3036
const BookmarksPage = () => {
3137
const { _t } = useTranslation();
3238
const feedInfiniteQuery = useInfiniteQuery({
@@ -35,15 +41,16 @@ const BookmarksPage = () => {
3541
myBookmarks({ limit: 10, page: pageParam, offset: 0 }),
3642
initialPageParam: 1,
3743
getNextPageParam: (lastPage) => {
38-
const _page = lastPage?.meta?.currentPage ?? 1;
39-
const _totalPages = lastPage?.meta?.totalPages ?? 1;
44+
if (!isBookmarksSuccess(lastPage)) return null;
45+
const _page = lastPage.meta.currentPage;
46+
const _totalPages = lastPage.meta.totalPages;
4047
return _page + 1 <= _totalPages ? _page + 1 : null;
4148
},
4249
});
4350

4451
const hasItems = useMemo(() => {
45-
const length = feedInfiniteQuery.data?.pages.flat()[0]?.nodes.length ?? 0;
46-
return length > 0;
52+
const firstOk = feedInfiniteQuery.data?.pages.find(isBookmarksSuccess);
53+
return (firstOk?.nodes.length ?? 0) > 0;
4754
}, [feedInfiniteQuery]);
4855

4956
const appConfirm = useAppConfirm();
@@ -65,8 +72,9 @@ const BookmarksPage = () => {
6572
<article key={i} className=" bg-muted h-20 animate-pulse" />
6673
))}
6774

68-
{feedInfiniteQuery.data?.pages.map((page) => {
69-
return page?.nodes.map((bookmark) => (
75+
{feedInfiniteQuery.data?.pages.flatMap((page) => {
76+
if (!isBookmarksSuccess(page)) return [];
77+
return page.nodes.map((bookmark) => (
7078
<article
7179
key={bookmark.id}
7280
className="flex justify-between flex-col md:flex-row py-3 space-y-2"

src/backend/services/bookmark.action.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export async function toggleResourceBookmark(
5555
]);
5656
return { bookmarked: true };
5757
} catch (error) {
58-
handleActionException(error);
58+
return handleActionException(error);
5959
}
6060
}
6161

@@ -126,7 +126,7 @@ export async function myBookmarks(
126126
},
127127
};
128128
} catch (error) {
129-
handleActionException(error);
129+
return handleActionException(error);
130130
}
131131
}
132132

@@ -160,5 +160,6 @@ export async function bookmarkStatus(
160160
return { bookmarked: Boolean(existingBookmark) };
161161
} catch (error) {
162162
handleActionException(error);
163+
return { bookmarked: false };
163164
}
164165
}

src/backend/services/gist.actions.ts

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -125,20 +125,16 @@ export async function getGist(
125125
],
126126
});
127127

128-
// check visibility
129-
if (!gistFindResponse[0].is_public) {
130-
if (gistFindResponse[0].owner_id !== sessionUserId) {
131-
throw new ActionException("Not authorized to view this gist");
132-
}
128+
const row = gistFindResponse[0];
129+
if (!row) {
130+
throw new ActionException("Gist not found");
133131
}
134132

135-
if (gistFindResponse[0]) {
136-
gist = gistFindResponse[0];
133+
if (!row.is_public && row.owner_id !== sessionUserId) {
134+
throw new ActionException("Not authorized to view this gist");
137135
}
138136

139-
if (!gist) {
140-
throw new ActionException("Gist not found");
141-
}
137+
gist = row;
142138

143139
const gistFiles = await persistenceRepository.gistFile.find({
144140
where: eq("gist_id", gist.id),

src/backend/services/reaction.actions.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,24 @@ import { ReactionStatus } from "../models/domain-models";
1111

1212
const sql = String.raw;
1313

14+
const REACTION_TYPES = [
15+
"LOVE",
16+
"UNICORN",
17+
"WOW",
18+
"FIRE",
19+
"CRY",
20+
"HAHA",
21+
] as const;
22+
23+
function emptyReactionStatuses(): ReactionStatus[] {
24+
return REACTION_TYPES.map((reaction_type) => ({
25+
reaction_type,
26+
count: 0,
27+
is_reacted: false,
28+
reactor_user_ids: [],
29+
}));
30+
}
31+
1432
export async function toogleReaction(
1533
_input: z.infer<typeof ReactionActionInput.toggleReactionInput>
1634
) {
@@ -66,7 +84,7 @@ export async function toogleReaction(
6684
is_reacted: true,
6785
};
6886
} catch (error) {
69-
handleActionException(error);
87+
return handleActionException(error);
7088
}
7189
}
7290

@@ -117,18 +135,17 @@ export async function getResourceReactions(
117135
}
118136

119137
// Return all types, filling missing ones with count: 0
120-
return ["LOVE", "UNICORN", "WOW", "FIRE", "CRY", "HAHA"].map(
121-
(reaction_type) => {
122-
const entry = reactionMap.get(reaction_type);
123-
return {
124-
reaction_type,
125-
count: entry?.count ?? 0,
126-
is_reacted: entry?.is_reacted ?? false,
127-
reactor_user_ids: entry?.reactor_user_ids ?? [],
128-
};
129-
}
130-
);
138+
return REACTION_TYPES.map((reaction_type) => {
139+
const entry = reactionMap.get(reaction_type);
140+
return {
141+
reaction_type,
142+
count: entry?.count ?? 0,
143+
is_reacted: entry?.is_reacted ?? false,
144+
reactor_user_ids: entry?.reactor_user_ids ?? [],
145+
};
146+
});
131147
} catch (error) {
132148
handleActionException(error);
149+
return emptyReactionStatuses();
133150
}
134151
}

0 commit comments

Comments
 (0)