diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 0000000..7239d7d --- /dev/null +++ b/.eslintrc.json @@ -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" + } +} + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d43c70f --- /dev/null +++ b/AGENTS.md @@ -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. diff --git a/app/(main)/blogs/[slug]/page.tsx b/app/(main)/blogs/[slug]/page.tsx new file mode 100644 index 0000000..04928a6 --- /dev/null +++ b/app/(main)/blogs/[slug]/page.tsx @@ -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; +}; + +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 ( + + ); +} + +export default Page; diff --git a/app/(main)/blogs/create/page.tsx b/app/(main)/blogs/create/page.tsx new file mode 100644 index 0000000..5ccf493 --- /dev/null +++ b/app/(main)/blogs/create/page.tsx @@ -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([]); + + const form = useForm>({ + resolver: zodResolver(BlogPostValidator), + defaultValues: { title: "", content: "" } + }); + + const onSubmit: SubmitHandler> = 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 ( + }> +
+
+

Write a Blog Post

+

+ Share insights, lessons learned, and tutorials with the community. +

+
+
+ + ( + + Title + + + + + + )} + /> + + ( + + Content + +