Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
25a786f
Add username generation logic during user sign-in
saifulshihab Sep 15, 2025
b575c8b
Add "Profile Not Found" page component
saifulshihab Sep 15, 2025
fafec84
Fetch user data in AppSidebar and display username with loading state
saifulshihab Sep 15, 2025
e56da6c
Add username availability check and debounce functionality in profile…
saifulshihab Sep 15, 2025
06d635e
add installation guide
saifulshihab Sep 21, 2025
cb7f4fc
Dockerize + gh actions (#10)
saifulshihab Nov 10, 2025
aa35ab0
User profile category flow (#11)
saifulshihab Nov 15, 2025
9bc4e6e
trigger build test action dev branch push
saifulshihab Nov 15, 2025
fd4464c
Update session handling in main layout
saifulshihab Nov 15, 2025
c0226d9
remove username from profile dropdown
saifulshihab Nov 15, 2025
a03c6ef
split prisma models into separate files and add prisma configuration …
saifulshihab Nov 15, 2025
4598163
fix user navigation link (#15)
saifulshihab Nov 27, 2025
18b9b01
Jobs functionalities - List, Create, Apply (#14)
saifulshihab Dec 6, 2025
ba6a740
add next js top loader (#18)
saifulshihab Dec 6, 2025
f69e009
Refactor question components and enhance UI
saifulshihab Dec 6, 2025
9001ad5
improve styles
saifulshihab Dec 6, 2025
31a0373
add job page layout for consistency with questions page design
saifulshihab Dec 6, 2025
ceae11d
validate user data using zod's `safeParse` method (#19)
saifulshihab Dec 6, 2025
f307737
add gradient bg style
saifulshihab Dec 7, 2025
a6fca1b
hide top bar loader spinner
saifulshihab Dec 7, 2025
45edbfc
optimize question queries
saifulshihab Dec 7, 2025
fda48be
add typecheck command
saifulshihab Dec 7, 2025
bc0983f
refactor: replace MessageCircle with MessageCircleQuestionMark and ad…
saifulshihab Dec 7, 2025
63f801a
update gradient color
saifulshihab Dec 10, 2025
c40b48b
update query
saifulshihab Dec 10, 2025
d3f2713
add links
saifulshihab Dec 10, 2025
ddafb7b
display joining date
saifulshihab Dec 10, 2025
460c051
simplify action imports by consolidating job-actions
saifulshihab Dec 10, 2025
a2b06d3
add ask question button
saifulshihab Jan 4, 2026
f85b396
Snippet feature (#21)
saifulshihab Mar 22, 2026
5b11c24
feat: add blog functionality with CRUD operations
saifulshihab May 1, 2026
b1760c5
Merge branch 'main' into dev
saifulshihab May 1, 2026
f39a400
fix div
saifulshihab May 1, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": ["next/core-web-vitals", "next/typescript"],
"rules": {
"@typescript-eslint/no-unused-vars": [
"warn",
{
"args": "all",
"argsIgnorePattern": "^_"
}
],
"@typescript-eslint/no-explicit-any": "off",
"no-console": "warn"
}
}

47 changes: 47 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Repository Guidelines

## Project Structure & Module Organization

- `app/`: Next.js App Router routes, layouts, and server actions.
- `components/`: React UI components, organized by feature (e.g. `components/job/`, `components/question/`). Files use `kebab-case.tsx`.
- `lib/`: Shared utilities (helpers, DB/query helpers, constants).
- `prisma/`: Prisma schema and migrations (`prisma/schema.prisma`, `prisma/migrations/`, `prisma/models/`).
- `public/`: Static assets served by Next.js.
- `types/`: Shared TypeScript types.
- Root config: `next.config.mjs`, `tailwind.config.ts`, `eslint.config.mjs`, `.prettierrc`.

## Build, Test, and Development Commands

- `npm install`: Install dependencies.
- `npm run dev`: Start local dev server (Next.js).
- `npm run build`: Production build.
- `npm run start`: Serve the production build locally.
- `npm run lint`: Run Next.js/ESLint checks.
- `npm run typecheck`: Run TypeScript (`tsc --noEmit`).
- Prisma:
- `npx prisma generate`: Generate Prisma client.
- `npx prisma migrate dev`: Create/apply migrations in development.
- `npx prisma migrate deploy`: Apply existing migrations (CI/production).

## Coding Style & Naming Conventions

- TypeScript + React (`.ts`/`.tsx`), 2-space indentation.
- Prettier is the source of truth (`printWidth: 80`, semicolons on, no trailing commas) with `prettier-plugin-tailwindcss`.
- ESLint extends `next/core-web-vitals` + `next/typescript` (unused vars allowed when prefixed with `_`).
- Prefer feature folders and small components over monolith files.

## Testing Guidelines

- No dedicated test runner is configured yet (no `jest`/`vitest` scripts). For now, treat `npm run lint` and `npm run typecheck` as the required safety net.
- If you add tests, keep them colocated (e.g. `components/job/job-card.test.tsx`) and add a corresponding `npm run test` script.

## Commit & Pull Request Guidelines

- Commits are typically short, imperative, and feature-focused (examples: `add typecheck command`, `refactor: ...`). Keep subjects under ~72 chars; optionally include a prefix like `refactor:`/`fix:`.
- PRs should include: a clear description, linked issue/PR number when applicable, and screenshots/GIFs for UI changes.
- Include Prisma migration notes in the PR when schema changes are involved (`prisma/migrations/*`).

## Security & Configuration Tips

- Never commit secrets. Use `.env.example` as the contract for required variables and keep local values in `.env`.
- Be careful with auth/session changes (`auth.ts`, `middleware.ts`): call out behavior changes in PR descriptions.
33 changes: 33 additions & 0 deletions app/(main)/blogs/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { authCheck } from "@/auth";
import BlogDetail from "@/components/blog/blog-detail";
import { getBlogComments, getBlogPostBySlug } from "@/lib/actions";
import { notFound } from "next/navigation";

type Props = {
params: Promise<{ slug: string }>;
searchParams: Record<string, string | undefined>;
};

async function Page(props: Props) {
const slug = (await props.params).slug;
const post = await getBlogPostBySlug(slug);
const creatorView = props.searchParams.creatorView === "true";

if (!post || "error" in post) notFound();

const commentsResult = await getBlogComments(post.id);
const comments = commentsResult?.comments ?? [];

const { isAuthenticated } = await authCheck();

return (
<BlogDetail
post={post}
comments={comments}
canComment={isAuthenticated}
creatorView={creatorView}
/>
);
}

export default Page;
159 changes: 159 additions & 0 deletions app/(main)/blogs/create/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
"use client";

import { Button } from "@/components/ui/button";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import Spinner from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";
import { createBlogPost } from "@/lib/actions";
import { BlogPostValidator } from "@/lib/validators/blog-validator";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon, X } from "lucide-react";
import { useRouter } from "nextjs-toploader/app";
import { Suspense, useState } from "react";
import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";

function Page() {
const router = useRouter();
const [isLoading, setIsLoading] = useState(false);
const [tags, setTags] = useState<string[]>([]);

const form = useForm<z.infer<typeof BlogPostValidator>>({
resolver: zodResolver(BlogPostValidator),
defaultValues: { title: "", content: "" }
});

const onSubmit: SubmitHandler<z.infer<typeof BlogPostValidator>> = async (
data
) => {
try {
setIsLoading(true);
const res = await createBlogPost(data, tags);
if (res?.error) {
toast.error(res.error);
return;
}
if (res?.post) {
router.push(`/blogs/${res.post.slug}`);
form.reset();
}
} catch {
toast.error("Something went wrong");
} finally {
setIsLoading(false);
}
};

return (
<Suspense fallback={<Spinner />}>
<div className="rounded-xl border border-zinc-800 bg-zinc-900">
<div className="mb-6 rounded-t-xl border-b border-zinc-800 bg-gradient-to-r from-zinc-900 to-zinc-800/50 p-4 text-center">
<h2 className="text-2xl font-bold">Write a Blog Post</h2>
<p className="mt-2 text-sm text-zinc-500">
Share insights, lessons learned, and tutorials with the community.
</p>
</div>
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-6 p-4"
>
<FormField
name="title"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Title</FormLabel>
<FormControl>
<Input {...field} placeholder="e.g. Prisma tips for Next.js" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<FormField
name="content"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>Content</FormLabel>
<FormControl>
<Textarea
rows={14}
{...field}
placeholder="Write your post here..."
className="text-sm"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>

<div className="space-y-2">
<FormLabel>Tags</FormLabel>
<div className="grid grid-cols-4 items-center gap-3">
{tags.map((tag, tagIndex) => (
<div key={tagIndex} className="group col-span-1 flex">
<Input
value={tag}
placeholder="Tag"
onChange={(e) => {
const newTags = [...tags];
newTags[tagIndex] = e.target.value;
setTags(newTags);
}}
/>
<Button
type="button"
onClick={() =>
setTags(tags.filter((_, i) => i !== tagIndex))
}
variant="ghost"
className="-translate-x-1 transform opacity-0 transition group-hover:translate-x-1 group-hover:opacity-100 hover:bg-transparent active:scale-95"
size="icon"
>
<X />
</Button>
</div>
))}
<Button
type="button"
variant="secondary"
className="w-[8.9375rem] border-dashed"
onClick={() => setTags([...tags, ""])}
>
<PlusIcon />
Add Tag
</Button>
</div>
</div>

<Separator />
<Button
type="submit"
isLoading={isLoading}
className="w-full py-2 font-semibold"
>
Publish
</Button>
</form>
</Form>
</div>
</Suspense>
);
}

export default Page;

31 changes: 31 additions & 0 deletions app/(main)/blogs/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { authCheck } from "@/auth";
import { Button } from "@/components/ui/button";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import React from "react";

export default async function Layout({
children
}: Readonly<{ children: React.ReactNode }>) {
const { isAuthenticated } = await authCheck();

return (
<div>
<div className="flex h-[3.125rem] items-center justify-between border-b border-dashed px-4">
<h1 className="text-2xl font-semibold leading-none">Blogs</h1>
{isAuthenticated && (
<Button asChild variant="outline">
<Link href="/blogs/create">
<PlusIcon size={14} />
New Blog
</Link>
</Button>
)}
</div>
<div className="h-[calc(100vh-3.125rem)] overflow-y-auto p-3">
{children}
</div>
</div>
);
}

41 changes: 41 additions & 0 deletions app/(main)/blogs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import BlogList from "@/components/blog/blog-list";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { getBlogPosts } from "@/lib/actions";
import { Search } from "lucide-react";

type SearchParams = { search?: string };

async function Page({ searchParams }: { searchParams: SearchParams }) {
const { search } = searchParams;

const result = await getBlogPosts({
search: search?.trim() || undefined
});
const posts = result?.posts ?? [];

return (
<div className="flex flex-col gap-4">
<div>
<form className="flex items-center gap-2" method="GET">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Input
name="search"
defaultValue={search}
placeholder="Search blogs..."
className="h-9 border-dashed bg-muted/50 pl-9 text-sm"
/>
</div>
<Button type="submit" variant="secondary" size="sm" className="h-9">
Filter
</Button>
</form>
</div>

<BlogList posts={posts} />
</div>
);
}

export default Page;
2 changes: 1 addition & 1 deletion app/(main)/questions/[id]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ async function Page(props: Props) {
if (!question) notFound();

return (
<div className="space-y-6">
<div className="space-y-3">
{/* Back Button */}
<Button
asChild
Expand Down
8 changes: 8 additions & 0 deletions app/(main)/user/activity/blogs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import UserBlogs from "@/components/user/activity/blogs";

async function Page() {
return <UserBlogs />;
}

export default Page;

9 changes: 8 additions & 1 deletion components/app-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Briefcase,
ChevronsUpDown,
Code,
FileText,
HelpCircle,
Home,
LogIn,
Expand Down Expand Up @@ -119,6 +120,12 @@ export function AppSidebar() {
icon: <Briefcase size={16} />,
href: "/jobs",
badge: null
},
{
text: "Blogs",
icon: <FileText size={16} />,
href: "/blogs",
badge: null
}
];

Expand Down Expand Up @@ -305,7 +312,7 @@ export function AppSidebar() {
{/* Main Navigation */}
<div
className={cn("h-[calc(100vh-10.8125rem)] overflow-y-auto", {
"h-[calc(100vh-21.6875rem)]": isAuthenticated
"h-[calc(100vh-370px)]": isAuthenticated
})}
>
<div className="p-3">
Expand Down
Loading
Loading