diff --git a/.agents/skills/github-issue-create/SKILL.md b/.agents/skills/github-issue-create/SKILL.md new file mode 100644 index 00000000..33e71b6c --- /dev/null +++ b/.agents/skills/github-issue-create/SKILL.md @@ -0,0 +1,105 @@ +--- +name: github-issue-create +description: >- + Creates a new GitHub issue using the GitHub CLI (gh issue create): title, + body, labels, assignees, milestone, template, or target repo. Use when the + user asks to open, file, or create a GitHub issue via gh; wants gh issue new; + or says to track work on GitHub without an existing issue number. +--- + +# GitHub issue create (gh CLI) + +## When this applies + +Use when the user wants a **new** issue recorded on GitHub and agrees to use **`gh`** (not only local notes). If they only want a draft, produce markdown they can paste instead of running `gh`. + +## Prerequisites + +- `gh` installed and authenticated: `gh auth status` (else `gh auth login`). +- Network access when running `gh`. +- Prefer the **git repo root** so the default remote matches the intended repository. +- Another repo: pass `-R owner/repo` (or `HOST/owner/repo` for GitHub Enterprise). + +## 1. Gather content + +Before running `gh`, confirm: + +- **Title** — short, imperative or problem-focused (e.g. “Profile articles tab shows blank when empty”). +- **Body** — context, steps to reproduce, expected vs actual, screenshots/links as markdown. Do **not** paste secrets, tokens, or full `.env` contents. + +Optional metadata from the user: + +- **Labels** (`-l`): repeat per label or comma-separated per `gh` examples. +- **Assignee** (`-a`): logins or `@me`. +- **Milestone** (`-m`): exact name as on GitHub. +- **Project** (`-p`): project title; requires `gh auth refresh -s project` if GitHub returns project permission errors. +- **Template** (`-T`): must match a template filename in `.github/ISSUE_TEMPLATE/` (if the repo uses them). + +## 2. Create the issue + +**Simple (inline):** + +```bash +gh issue create --title "Your title" --body "Your body (markdown OK in quotes; use a file for long text)." +``` + +**Long body or complex markdown — use a file:** + +```bash +gh issue create --title "Your title" --body-file path/to/issue-body.md +``` + +**Stdin:** + +```bash +gh issue create --title "Your title" --body-file - +# then paste body, end with Ctrl-D +``` + +**Labels and self-assign:** + +```bash +gh issue create -t "Title" -b "Body" -l bug -l "help wanted" -a "@me" +``` + +**Editor (title + body in one buffer — first line is title):** + +```bash +gh issue create -e +``` + +**Web UI:** + +```bash +gh issue create -w +``` + +**Explicit repo:** + +```bash +gh issue create -R owner/repo -t "Title" -b "Body" +``` + +**From template:** + +```bash +gh issue create --template "Bug Report" +``` + +Alias: `gh issue new` is equivalent to `gh issue create`. + +## 3. After create + +`gh` prints the new issue URL. Share it with the user. Optionally mention the related skill for fixing issues: `github-issue-resolve`. + +## Safety + +- Do **not** run `gh issue create` without a clear title and body the user (or task) actually wants on the public tracker. +- If intent is ambiguous, ask once for title, body, and labels before creating. + +## Checklist + +- [ ] Title and body ready; no secrets in the body. +- [ ] Correct repo (`cwd` or `-R`). +- [ ] Labels / assignee / milestone / project / template applied if requested. +- [ ] Command run; URL returned to the user. diff --git a/.agents/skills/github-issue-resolve/SKILL.md b/.agents/skills/github-issue-resolve/SKILL.md new file mode 100644 index 00000000..ca55dde4 --- /dev/null +++ b/.agents/skills/github-issue-resolve/SKILL.md @@ -0,0 +1,102 @@ +--- +name: github-issue-resolve +description: >- + Resolves a numbered GitHub issue using the GitHub CLI (gh): fetch full + context, implement the fix, then post a structured summary comment on the + issue. Use when the user asks to fix, solve, or implement a GitHub issue by + number; mentions gh issue view or GitHub CLI for issues; or says to comment + back on the issue after fixing. +--- + +# GitHub issue resolve (gh CLI) + +## When this applies + +Use this workflow when the user gives an issue number (e.g. “fix #57”, “solve issue 42”) and expects the work tracked on GitHub with a **comment**, not only local code changes. + +## Prerequisites + +- `gh` installed and authenticated: `gh auth status` (if it fails, run `gh auth login`). +- Shell with `network` permission when calling `gh`. +- Run `gh` from the **git repo root** so `gh issue view` targets the correct repository. + +## 1. Load the issue + +Always fetch the issue before coding so title, body, labels, and images/links are grounded in real data. + +```bash +gh issue view +``` + +Useful additions: + +```bash +gh issue view --json title,body,state,labels,assignees,comments --jq . +``` + +If discussion matters: + +```bash +gh issue view --comments +``` + +**Images in the body**: GitHub stores them as URLs; open or fetch if the visual is required to understand the bug. + +## 2. Implement the fix + +- Follow the repo’s normal patterns (read surrounding code first; minimal diff). +- Run targeted checks (e.g. `eslint` on touched files, `bun run build` if appropriate). Fix anything **introduced** by the change. + +## 3. Summary for GitHub (before commenting) + +Draft a short, professional summary the maintainer can skim: + +- **What** was wrong (1–2 sentences, tied to the issue). +- **What changed** (bullet list of files or areas; no huge pasted code). +- **How to verify** (e.g. steps or pages to open). +- Optional: **Follow-ups** only if truly out of scope. + +Do **not** include secrets, tokens, `.env` values, or internal-only URLs in the comment. + +## 4. Post the comment on the issue + +Post the summary as an **issue comment** (does not close the issue unless the user asked to close it). + +**Option A — body from stdin:** + +```bash +gh issue comment --body "$(cat <<'EOF' +## Summary + +[your markdown here] +EOF +)" +``` + +**Option B — file:** + +Write the body to a temp file, then: + +```bash +gh issue comment --body-file /path/to/comment.md +``` + +Prefer markdown headings (`##`) and bullets for readability. + +## 5. Close the issue (only if asked) + +If the user explicitly wants the issue closed after the fix: + +```bash +gh issue close --comment "Fixed in [branch/PR description]. See comment above for details." +``` + +Otherwise leave it **open** for human review or PR merge. + +## Checklist + +- [ ] Fetched issue with `gh issue view` (and JSON/comments if needed). +- [ ] Implemented and validated the change locally. +- [ ] Wrote a concise summary (no secrets). +- [ ] Posted `gh issue comment `. +- [ ] Closed the issue only if the user requested it. diff --git a/CHANGELOG.md b/CHANGELOG.md index ff5caff6..766fb823 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,21 @@ This project adheres to [Semantic Versioning](https://semver.org/). --- +## v1.5.0 — 2026-04-01 + +### ✨ Features +- feat: enhance NavbarActions with dropdown menu for creating new entries (eb652b2) +- feat: add unpublished article notice and draft byline support (4528d1a) +- feat: enhance profile page with additional user information and improved article loading states (2458ad7) +- feat: improve user profile experience with enhanced data display and loading states (3dedc30) +- feat: enhance user profile with additional fields and improved loading states (2db6598) +- feat: improve user avatar handling and display in LatestUsers component (ea1fe57) + +### 🔧 Other Changes +- refactor: update theme handling and improve layout structure (524ebc4) + +--- + ## v1.4.0 — 2026-03-30 ### ✨ Features diff --git a/package.json b/package.json index bb87c9b4..8820ca2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "techdiary.dev-next", - "version": "1.4.0", + "version": "1.5.0", "private": true, "scripts": { "dev": "next dev --turbo", @@ -58,7 +58,6 @@ "meilisearch": "^0.51.0", "modern-screenshot": "^4.6.8", "next": "^16.2.1", - "next-themes": "^0.4.6", "pg": "^8.14.1", "react": "^19", "react-advanced-cropper": "^0.20.1", diff --git a/src/app/[username]/(profile-page)/_components/ProfilePageAside.tsx b/src/app/[username]/(profile-page)/_components/ProfilePageAside.tsx index 6cefc5a3..9fbfc867 100644 --- a/src/app/[username]/(profile-page)/_components/ProfilePageAside.tsx +++ b/src/app/[username]/(profile-page)/_components/ProfilePageAside.tsx @@ -43,6 +43,13 @@ const ProfilePageAside: React.FC = ({ profile }) => {

{profile?.bio}

)} + {profile?.skills && ( +

+ Skills: + {profile.skills} +

+ )} + {/* User infos start */}
{profile?.website_url && ( diff --git a/src/app/[username]/(profile-page)/articles/UserArticleFeed.tsx b/src/app/[username]/(profile-page)/articles/UserArticleFeed.tsx index d6ce4741..ec6c1eb2 100644 --- a/src/app/[username]/(profile-page)/articles/UserArticleFeed.tsx +++ b/src/app/[username]/(profile-page)/articles/UserArticleFeed.tsx @@ -1,18 +1,22 @@ "use client"; -import { useInfiniteQuery, useQuery } from "@tanstack/react-query"; +import { useInfiniteQuery } from "@tanstack/react-query"; import * as articleActions from "@/backend/services/article.actions"; import React, { useMemo } from "react"; import ArticleCard from "@/components/ArticleCard"; import { readingTime } from "@/lib/utils"; import VisibilitySensor from "@/components/VisibilitySensor"; import getFileUrl from "@/utils/getFileUrl"; +import { useTranslation } from "@/i18n/use-translation"; +import Image from "next/image"; interface UserArticleFeedProps { userId: string; } const UserArticleFeed: React.FC = ({ userId }) => { + const { _t } = useTranslation(); + const feedInfiniteQuery = useInfiniteQuery({ queryKey: ["user-article-feed", userId], queryFn: ({ pageParam }) => @@ -42,12 +46,17 @@ const UserArticleFeed: React.FC = ({ userId }) => { }); const feedArticles = useMemo(() => { - return feedInfiniteQuery.data?.pages.flatMap((page) => page?.nodes); + return feedInfiniteQuery.data?.pages.flatMap((page) => page?.nodes) ?? []; }, [feedInfiniteQuery.data]); + const showEmpty = + !feedInfiniteQuery.isPending && + !feedInfiniteQuery.isError && + feedArticles.length === 0; + return ( <> - {feedInfiniteQuery.isFetching && ( + {feedInfiniteQuery.isPending && (
{Array.from({ length: 6 }).map((_, index) => (
@@ -55,9 +64,31 @@ const UserArticleFeed: React.FC = ({ userId }) => {
)} - {/*
{JSON.stringify(feedArticles, null, 2)}
*/} + {feedInfiniteQuery.isError && ( +
+ {_t("Could not load articles.")} +
+ )} + + {showEmpty && ( +
+ +

+ {_t("No articles published yet")} +

+

+ {_t("When this user publishes articles, they will show up here.")} +

+
+ )} - {feedArticles?.map((article) => ( + {feedArticles.map((article) => ( = async ({ params }) => { const username = sanitizedUsername(_params?.username); const profile = await getUserByUsername(username, ["id", "username"]); + if (!profile?.id) { + return null; + } + return (
- {/*
{JSON.stringify(profile, null, 2)}
*/} - +
); }; diff --git a/src/app/[username]/(profile-page)/layout.tsx b/src/app/[username]/(profile-page)/layout.tsx index d749395f..ad4eb402 100644 --- a/src/app/[username]/(profile-page)/layout.tsx +++ b/src/app/[username]/(profile-page)/layout.tsx @@ -47,15 +47,21 @@ const layout: React.FC = async ({ const _params = await params; const username = sanitizedUsername(_params?.username); const profile = await getUserByUsername(username, [ - // all fields "id", "name", "username", "email", "profile_photo", + "bio", + "designation", + "website_url", + "education", + "location", + "social_links", + "skills", + "is_verified", "created_at", "updated_at", - "social_links", ]); if (!profile) { diff --git a/src/app/[username]/[articleHandle]/_components/UnpublishedArticleNotice.tsx b/src/app/[username]/[articleHandle]/_components/UnpublishedArticleNotice.tsx new file mode 100644 index 00000000..75fca6ba --- /dev/null +++ b/src/app/[username]/[articleHandle]/_components/UnpublishedArticleNotice.tsx @@ -0,0 +1,63 @@ +"use client"; + +import { Badge } from "@/components/ui/badge"; +import { useTranslation } from "@/i18n/use-translation"; +import { useSession } from "@/store/session.atom"; +import { FileWarning } from "lucide-react"; + +type Props = { + publishedAt: Date | null | undefined; + authorId: string; +}; + +/** Inline label next to the byline date when the article has no `published_at`. */ +export function ArticleDraftBylineLabel() { + const { _t } = useTranslation(); + return ( + + {_t("Draft")} + + ); +} + +export function UnpublishedArticleNotice({ publishedAt, authorId }: Props) { + const { _t } = useTranslation(); + const session = useSession(); + + if (publishedAt) return null; + + const viewerId = session?.session?.user_id; + const isAuthor = Boolean(viewerId) && viewerId === authorId; + + return ( +
+
+ + + {_t("Draft")} + + + {_t("Unpublished article")} + +
+

+ {isAuthor + ? _t( + "This draft is not listed anywhere on the site. Only people with the link can open it. Publish from the editor when you want it on your profile and in feeds.", + ) + : _t( + "This article is unpublished. It is not listed on the site; only people with this link can view it.", + )} +

+
+ ); +} diff --git a/src/app/[username]/[articleHandle]/page.tsx b/src/app/[username]/[articleHandle]/page.tsx index 4c38eb8c..2e3bfd60 100644 --- a/src/app/[username]/[articleHandle]/page.tsx +++ b/src/app/[username]/[articleHandle]/page.tsx @@ -24,6 +24,10 @@ import type { Article, WithContext } from "schema-dts"; import { eq } from "sqlkit"; import ArticleSidebar from "./_components/ArticleSidebar"; import EditArticleButton from "./_components/EditArticleButton"; +import { + ArticleDraftBylineLabel, + UnpublishedArticleNotice, +} from "./_components/UnpublishedArticleNotice"; interface ArticlePageProps { params: Promise<{ @@ -136,6 +140,10 @@ const Page: NextPage = async ({ params }) => { > {/* {!article &&
Article not found
} */}
+ {article?.cover_image && (
= async ({ params }) => { {article?.user?.name}
- + {article?.published_at ? ( + + ) : ( + + )} · {readingTime(article?.body ?? "")} min read
diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 5da45d3a..cf0c3312 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,11 +3,13 @@ import { AuthKitProvider } from "@workos-inc/authkit-nextjs/components"; import "../styles/app.css"; import CommonProviders from "@/components/providers/CommonProviders"; +import RootProviders from "@/components/providers/root-providers"; import LanguageHydrator from "@/components/providers/LanguageHydrator"; import SessionHydrator from "@/components/providers/SessionHydrator"; import { CookieConsentPopup } from "@/components/CookieConsentPopup"; import { fontKohinoorBanglaRegular } from "@/lib/fonts"; import { Toaster } from "@/components/toast"; +import { THEME_INIT_SCRIPT } from "@/lib/theme-init-script"; import Script from "next/script"; import React, { PropsWithChildren, Suspense } from "react"; @@ -41,6 +43,9 @@ const RootLayout: React.FC = ({ children }) => { return ( +