From 764e2c7dced8169eccf327366eba6b2452664ac5 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 3 Jan 2026 19:26:57 +0400 Subject: [PATCH 1/9] docs: add database strategy report for DevFlow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notes: - Analyses current mock data patterns (DAL vs hard-coded) - Compares MongoDB, Neon PostgreSQL, and Convex - Recommends Convex for AI-assisted development and real-time features - Documents migration path from mock data to production database Planning documentation for transitioning from mock data to a real database. Covers pricing, feature comparisons, and integration considerations with Clerk and Vercel. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- x_docs/my_notes/what-db.md | 525 +++++++++++++++++++++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100644 x_docs/my_notes/what-db.md diff --git a/x_docs/my_notes/what-db.md b/x_docs/my_notes/what-db.md new file mode 100644 index 0000000..8945024 --- /dev/null +++ b/x_docs/my_notes/what-db.md @@ -0,0 +1,525 @@ +# DevFlow Database Strategy Report ๐Ÿ“Š + +> A comprehensive guide for an intelligent adult beginner on mock data patterns and database choices for your Stack Overflow-like Q&A platform. + +--- + +## Table of Contents + +1. [Current State Analysis](#current-state-analysis) +2. [Mock Data Recommendations](#mock-data-recommendations) +3. [Database Options Compared](#database-options-compared) +4. [Deep Dive: MongoDB](#deep-dive-mongodb-) +5. [Deep Dive: Neon PostgreSQL](#deep-dive-neon-postgresql-) +6. [Deep Dive: Convex](#deep-dive-convex-) +7. [Final Recommendation](#final-recommendation-) + +--- + +## Current State Analysis + +### What You Have Now ๐Ÿ” + +Your codebase has two patterns for mock data: + +| File | Pattern | Status | +|------|---------|--------| +| `lib/data/questions.ts` | โœ… Async function `getTopQuestions()` | Good - abstracted | +| `lib/data/tags.ts` | โœ… Async function `getPopularTags()` | Good - abstracted | +| `components/right-sidebar/right-sidebar.tsx` | โœ… Uses the async functions | Good - ready for DB | +| `app/(root)/page.tsx` | โŒ Hard-coded `[1, 2, 3, 4].map()` | Bad - needs fixing | + +### The Problem + +Your **right sidebar** is already using the correct pattern: + +```typescript +// โœ… Right sidebar - GOOD pattern +const [topQuestions, popularTags] = await Promise.all([ + getTopQuestions(5), + getPopularTags(5), +]); +``` + +But your **homepage** completely ignores the mock data: + +```typescript +// โŒ Homepage - BAD pattern (hard-coded) +{[1, 2, 3, 4].map((questionIndex) => ( +
+

How to implement a sticky sidebar...

// Same question 4 times! +
+))} +``` + +### Your Tag Relation Concern + +You asked about the relationship between questions and tags. Currently: + +- `Question` type has only `{ _id, title }` - no tags! +- `Tag` type has `{ name, questions: number }` - just a count + +**This is a valid concern.** Real questions need associated tags. We'll address this in the recommendations. + +--- + +## Mock Data Recommendations + +### 1๏ธโƒฃ Create a Data Access Layer (DAL) + +Create a `lib/data/index.ts` that exports all your data functions. This becomes the **single source of truth** that you'll swap out later: + +```typescript +// lib/data/index.ts +export { getTopQuestions, type Question } from './questions'; +export { getPopularTags, type Tag } from './tags'; +export { getAllQuestions } from './questions'; // New function for homepage +``` + +### 2๏ธโƒฃ Expand Your Types to Match Real Data + +```typescript +// lib/data/types.ts +export type Tag = { + _id: string; + name: string; + description?: string; + questionsCount: number; +}; + +export type Question = { + _id: string; + title: string; + content: string; + tags: string[]; // Array of tag names + votes: number; + answersCount: number; + views: number; + author: { + _id: string; + name: string; + avatar?: string; + }; + createdAt: Date; +}; +``` + +### 3๏ธโƒฃ Create Richer Mock Data + +```typescript +// lib/data/questions.ts +const MOCK_QUESTIONS: Question[] = [ + { + _id: "1", + title: "How to Ensure Unique User Profile with ON CONFLICT in PostgreSQL?", + content: "I'm trying to understand how to handle upserts...", + tags: ["postgres", "nextjs"], + votes: 42, + answersCount: 3, + views: 1247, + author: { _id: "u1", name: "Alice Dev", avatar: "/avatars/alice.jpg" }, + createdAt: new Date("2025-12-28"), + }, + // ... more questions +]; +``` + +### 4๏ธโƒฃ Update Homepage to Use Data Functions + +```typescript +// app/(root)/page.tsx +import { getAllQuestions } from "@/lib/data"; + +const HomePage = async () => { + const questions = await getAllQuestions(); + + return ( + <> +

All Questions

+ {questions.map((question) => ( + + ))} + + ); +}; +``` + +### Does Database Choice Impact This? ๐Ÿค” + +**Short answer: No, not significantly.** + +The beauty of the Data Access Layer pattern is that your components don't care where data comes from. When you switch to a real database, you only change the implementation inside `lib/data/`: + +| Database | Change Required | +|----------|-----------------| +| MongoDB | Replace mock array with `db.collection('questions').find()` | +| Neon | Replace mock array with Prisma/Drizzle query | +| Convex | Replace mock array with `ctx.db.query("questions")` | + +Your components remain unchanged. This is called the **Repository Pattern**. + +--- + +## Database Options Compared + +### Quick Comparison Table ๐Ÿ“‹ + +| Feature | MongoDB ๐Ÿƒ | Neon ๐Ÿ˜ | Convex โšก | +|---------|-----------|---------|----------| +| **Type** | Document (NoSQL) | Relational (SQL) | Document-Relational | +| **Query Language** | MongoDB Query API | SQL | TypeScript functions | +| **Schema** | Flexible (optional) | Strict (migrations) | TypeScript code | +| **Real-time** | Change Streams (extra setup) | Not built-in | Built-in (automatic) | +| **Vercel Integration** | Marketplace | Marketplace + Branching | Marketplace | +| **Learning Curve** | Moderate | Low (if you know SQL) | Moderate (new paradigm) | +| **AI-Friendly** | Good | Good | Excellent | + +### Pricing Comparison ๐Ÿ’ฐ + +| Tier | MongoDB Atlas | Neon | Convex | +|------|---------------|------|--------| +| **Free** | 512 MB storage | 0.5 GB storage, 100 CU-hrs | 1M function calls, 0.5 GB | +| **Starter** | $8-30/mo (Flex) | $5/mo minimum | Pay-as-you-go | +| **Production** | ~$57/mo (M10) | ~$20-50/mo | $25/developer/mo | +| **Scale-to-Zero** | โŒ No | โœ… Yes | โœ… Yes | + +### Best Fit by Use Case ๐ŸŽฏ + +| If You Want... | Choose | +|----------------|--------| +| **Fastest development** | Convex (TypeScript all the way) | +| **Easiest AI coding** | Convex (Claude Code loves it) | +| **SQL familiarity** | Neon (it's just PostgreSQL) | +| **Flexible schema** | MongoDB or Convex | +| **Built-in real-time** | Convex | +| **Branch per PR** | Neon (automatic with Vercel) | +| **Most tutorials/resources** | MongoDB or PostgreSQL (Neon) | + +--- + +## Deep Dive: MongoDB ๐Ÿƒ + +### What Is It? + +MongoDB is a **document database** that stores data as JSON-like documents instead of rows in tables. Think of it like storing JavaScript objects directly in your database. + +### Document vs SQL Model + +```javascript +// MongoDB Document (one query, one document) +{ + _id: ObjectId("..."), + title: "How to centre a div?", + content: "I've tried everything...", + tags: ["css", "html", "flexbox"], // Embedded array + author: { // Embedded object + name: "Alice", + avatar: "/avatars/alice.jpg" + }, + answers: [ // Embedded array of objects + { content: "Use flexbox...", votes: 15 }, + { content: "Try grid...", votes: 8 } + ] +} + +// SQL Equivalent (multiple tables, JOINs required) +// questions table + users table + answers table + question_tags table +``` + +### Advantages โœ… + +1. **Natural for Q&A**: Questions with embedded answers/tags map perfectly +2. **Flexible schema**: Add fields without migrations +3. **Atlas Search**: Built-in full-text search for questions +4. **JSON native**: Documents are basically TypeScript objects + +### Disadvantages โŒ + +1. **No foreign keys**: Referential integrity is your responsibility +2. **JOINs are awkward**: `$lookup` is less natural than SQL JOINs +3. **16 MB document limit**: Very long threads might need restructuring +4. **Learning curve**: Different mental model from SQL + +### Is It SQL? + +**No.** MongoDB uses its own query language: + +```javascript +// MongoDB Query +db.questions.find({ tags: "nextjs" }).sort({ votes: -1 }).limit(10) + +// SQL Equivalent +SELECT * FROM questions +JOIN question_tags ON questions.id = question_tags.question_id +JOIN tags ON question_tags.tag_id = tags.id +WHERE tags.name = 'nextjs' +ORDER BY votes DESC LIMIT 10 +``` + +### Recommended for DevFlow? + +**Yes, it's a solid choice.** The document model fits Q&A platforms well. However, if you prefer SQL and relational integrity, consider Neon instead. + +--- + +## Deep Dive: Neon PostgreSQL ๐Ÿ˜ + +### What Is It? + +Neon is **serverless PostgreSQL** - the same PostgreSQL you know, but with superpowers: + +- **Scale-to-zero**: Database suspends when idle, costs nothing +- **Branching**: Instant copy of your database for each PR +- **Auto-scaling**: Resources adjust to demand + +### What Makes It "Serverless"? + +Traditional databases run 24/7 whether you use them or not. Neon: + +1. **Suspends after 5 minutes of inactivity** (configurable) +2. **Wakes up in ~200-500ms** when a query arrives +3. **Scales compute up/down** based on load +4. **Separates compute from storage** (pay separately) + +### Is It SQL? + +**Yes!** Neon is 100% PostgreSQL-compatible: + +```sql +SELECT q.*, array_agg(t.name) as tags +FROM questions q +JOIN question_tags qt ON q.id = qt.question_id +JOIN tags t ON qt.tag_id = t.id +WHERE t.name = 'nextjs' +GROUP BY q.id +ORDER BY q.votes DESC +LIMIT 10; +``` + +### The Branching Superpower ๐ŸŒฟ + +This is Neon's killer feature: + +``` +main (production database) + โ”œโ”€โ”€ staging + โ”œโ”€โ”€ feature-user-auth (your PR gets its own DB!) + โ””โ”€โ”€ feature-voting-system +``` + +**With Vercel integration:** + +- Open a PR โ†’ Neon automatically creates a database branch +- Merge the PR โ†’ Branch is deleted +- Test with production-like data without risk + +### Advantages โœ… + +1. **Full PostgreSQL**: All features, extensions, ORMs work +2. **Branching**: Each PR gets isolated database +3. **Scale-to-zero**: Development environments cost nothing +4. **Built-in connection pooling**: Works great with serverless +5. **Point-in-time recovery**: Restore to any moment + +### Disadvantages โŒ + +1. **Cold starts**: 200-500ms delay after idle periods +2. **Session state lost**: Temporary tables, prepared statements cleared on suspend +3. **Storage costs**: Large databases can get expensive +4. **Less flexible schema**: Migrations required for changes + +### ORM Choice: Prisma vs Drizzle + +| Aspect | Prisma | Drizzle | +|--------|--------|---------| +| **Maturity** | More mature, larger community | Newer, growing fast | +| **TypeScript** | Generated types | Native TypeScript schema | +| **Query Style** | Object-based | SQL-like | +| **Bundle Size** | Larger | Smaller | +| **Visual Tools** | Prisma Studio | Drizzle Studio | + +**My suggestion**: Drizzle is gaining popularity and has better TypeScript integration. But Prisma is excellent too. + +### Recommended for DevFlow? + +**Yes, excellent choice.** Especially if you value: + +- SQL familiarity +- Strong relational integrity (foreign keys) +- PR-based database branching +- Mature ecosystem (PostgreSQL) + +--- + +## Deep Dive: Convex โšก + +### What Is It? + +Convex is a **Backend Application Platform** - not just a database. It combines: + +- Document database +- Cloud functions (queries, mutations) +- **Automatic real-time sync** +- File storage, scheduling, search + +### Is It SQL? + +**No.** Convex uses **TypeScript functions** for everything: + +```typescript +// Convex Query (pure TypeScript) +export const getQuestionsByTag = query({ + args: { tagName: v.string() }, + handler: async (ctx, { tagName }) => { + return await ctx.db + .query("questions") + .withIndex("by_tag", (q) => q.eq("tagName", tagName)) + .order("desc") + .take(10); + }, +}); +``` + +### What Does "Everything in Code" Mean? ๐Ÿค– + +Your **entire backend** lives in a `convex/` folder as TypeScript: + +``` +convex/ +โ”œโ”€โ”€ schema.ts # Database schema (TypeScript) +โ”œโ”€โ”€ questions.ts # Query/mutation functions +โ”œโ”€โ”€ answers.ts # More functions +โ””โ”€โ”€ _generated/ # Auto-generated types +``` + +**Schema Example:** + +```typescript +// convex/schema.ts +export default defineSchema({ + questions: defineTable({ + title: v.string(), + content: v.string(), + authorId: v.id("users"), + votes: v.number(), + }) + .index("by_author", ["authorId"]) + .index("by_votes", ["votes"]), + + tags: defineTable({ + name: v.string(), + questionsCount: v.number(), + }).index("by_name", ["name"]), +}); +``` + +### Why Is This Good for AI Coding Agents? ๐Ÿค– + +| Traditional Stack | Convex | +|-------------------|--------| +| SQL files + ORM config + API routes + migrations | Just TypeScript | +| Context-switching between languages | One language | +| Schema in database, types separate | Schema IS the types | +| Manual API layer | Functions auto-exposed | + +**Claude Code benefits:** + +- Never writes SQL (TypeScript only) +- Full type safety - can validate its own code +- Schema and logic in same files - easy to understand +- Instant feedback with `npx convex dev` + +### The Real-Time Superpower โšก + +This is automatic - no WebSocket code needed: + +```typescript +// Frontend component +"use client"; +import { useQuery } from "convex/react"; +import { api } from "@/convex/_generated/api"; + +export function AnswerList({ questionId }) { + // This AUTOMATICALLY re-renders when answers change! + const answers = useQuery(api.answers.byQuestion, { questionId }); + + return answers?.map(a => ); +} +``` + +When someone posts an answer, **all users viewing that question see it instantly** - no polling, no manual refresh. + +### Advantages โœ… + +1. **TypeScript all the way**: No context-switching +2. **Real-time by default**: Answers appear instantly +3. **AI-friendly**: Claude Code excels at TypeScript +4. **Automatic caching**: Query results cached +5. **Transactional**: All mutations are atomic +6. **Vercel integration**: First-class support + +### Disadvantages โŒ + +1. **New paradigm**: Team must learn it +2. **No raw SQL**: Can't use psql, database tools +3. **Vendor lock-in**: Proprietary (though self-hosted option exists) +4. **Index requirements**: Must define indexes upfront +5. **Smaller ecosystem**: Fewer tutorials than PostgreSQL +6. **Complex aggregations**: No SQL GROUP BY - must code it + +### Recommended for DevFlow? + +**Yes, especially if:** + +- You want real-time features (live answers, votes) +- You're building with AI assistance (Claude Code) +- You prefer TypeScript over SQL +- You value developer experience over ecosystem maturity + +--- + +## Final Recommendation ๐ŸŽฏ + +### For Your Mock Data Now + +1. **Expand your types** to include tags on questions +2. **Create richer mock data** with realistic relationships +3. **Update the homepage** to use data functions (not hard-coded) +4. **Keep the Data Access Layer pattern** - it makes migration easy + +### For Database Choice + +Based on your situation (learning project, Vercel deployment, using Clerk, building with Claude Code): + +| Choice | When to Pick It | +|--------|-----------------| +| **๐Ÿฅ‡ Convex** | Best for AI-assisted development, real-time features, pure TypeScript | +| **๐Ÿฅˆ Neon** | Best for SQL familiarity, PR branching, relational data integrity | +| **๐Ÿฅ‰ MongoDB** | Best for flexible schema, document model, existing MongoDB experience | + +### My Top Pick: Convex โšก + +**Why?** + +1. **You're learning** - Convex's TypeScript-only approach is simpler +2. **You use Claude Code** - It excels at TypeScript, no SQL context-switching +3. **Q&A benefits from real-time** - Answers appearing instantly is valuable +4. **Vercel integration** - First-class support +5. **You already use Clerk** - Convex has documented Clerk integration + +**However**, if you prefer SQL and want the massive PostgreSQL ecosystem, **Neon is excellent** too. The PR branching feature is genuinely useful. + +--- + +## Next Steps ๐Ÿ“ + +1. โœ… Implement the Data Access Layer pattern (mock data abstraction) +2. โœ… Expand types to include question-tag relationships +3. โœ… Update homepage to use data functions +4. ๐Ÿ”œ Choose your database (I suggest trying Convex first) +5. ๐Ÿ”œ Replace mock data implementations with real database queries + +--- + +*Report generated for DevFlow Q&A Platform* +*Sources: Official documentation from MongoDB, Neon, Convex, and Vercel* From 7f07785526eb9c953f32973d4a5c317861f8a389 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 3 Jan 2026 21:21:26 +0400 Subject: [PATCH 2/9] docs: trim database strategy report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove implementation guidance, mock data patterns, and tutorial sections. Retain only the database comparison table and recommendation section. The document now focuses on decision-making rather than implementation detail. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- x_docs/my_notes/what-db.md | 191 +------------------------------------ 1 file changed, 1 insertion(+), 190 deletions(-) diff --git a/x_docs/my_notes/what-db.md b/x_docs/my_notes/what-db.md index 8945024..76955a2 100644 --- a/x_docs/my_notes/what-db.md +++ b/x_docs/my_notes/what-db.md @@ -1,163 +1,5 @@ # DevFlow Database Strategy Report ๐Ÿ“Š -> A comprehensive guide for an intelligent adult beginner on mock data patterns and database choices for your Stack Overflow-like Q&A platform. - ---- - -## Table of Contents - -1. [Current State Analysis](#current-state-analysis) -2. [Mock Data Recommendations](#mock-data-recommendations) -3. [Database Options Compared](#database-options-compared) -4. [Deep Dive: MongoDB](#deep-dive-mongodb-) -5. [Deep Dive: Neon PostgreSQL](#deep-dive-neon-postgresql-) -6. [Deep Dive: Convex](#deep-dive-convex-) -7. [Final Recommendation](#final-recommendation-) - ---- - -## Current State Analysis - -### What You Have Now ๐Ÿ” - -Your codebase has two patterns for mock data: - -| File | Pattern | Status | -|------|---------|--------| -| `lib/data/questions.ts` | โœ… Async function `getTopQuestions()` | Good - abstracted | -| `lib/data/tags.ts` | โœ… Async function `getPopularTags()` | Good - abstracted | -| `components/right-sidebar/right-sidebar.tsx` | โœ… Uses the async functions | Good - ready for DB | -| `app/(root)/page.tsx` | โŒ Hard-coded `[1, 2, 3, 4].map()` | Bad - needs fixing | - -### The Problem - -Your **right sidebar** is already using the correct pattern: - -```typescript -// โœ… Right sidebar - GOOD pattern -const [topQuestions, popularTags] = await Promise.all([ - getTopQuestions(5), - getPopularTags(5), -]); -``` - -But your **homepage** completely ignores the mock data: - -```typescript -// โŒ Homepage - BAD pattern (hard-coded) -{[1, 2, 3, 4].map((questionIndex) => ( -
-

How to implement a sticky sidebar...

// Same question 4 times! -
-))} -``` - -### Your Tag Relation Concern - -You asked about the relationship between questions and tags. Currently: - -- `Question` type has only `{ _id, title }` - no tags! -- `Tag` type has `{ name, questions: number }` - just a count - -**This is a valid concern.** Real questions need associated tags. We'll address this in the recommendations. - ---- - -## Mock Data Recommendations - -### 1๏ธโƒฃ Create a Data Access Layer (DAL) - -Create a `lib/data/index.ts` that exports all your data functions. This becomes the **single source of truth** that you'll swap out later: - -```typescript -// lib/data/index.ts -export { getTopQuestions, type Question } from './questions'; -export { getPopularTags, type Tag } from './tags'; -export { getAllQuestions } from './questions'; // New function for homepage -``` - -### 2๏ธโƒฃ Expand Your Types to Match Real Data - -```typescript -// lib/data/types.ts -export type Tag = { - _id: string; - name: string; - description?: string; - questionsCount: number; -}; - -export type Question = { - _id: string; - title: string; - content: string; - tags: string[]; // Array of tag names - votes: number; - answersCount: number; - views: number; - author: { - _id: string; - name: string; - avatar?: string; - }; - createdAt: Date; -}; -``` - -### 3๏ธโƒฃ Create Richer Mock Data - -```typescript -// lib/data/questions.ts -const MOCK_QUESTIONS: Question[] = [ - { - _id: "1", - title: "How to Ensure Unique User Profile with ON CONFLICT in PostgreSQL?", - content: "I'm trying to understand how to handle upserts...", - tags: ["postgres", "nextjs"], - votes: 42, - answersCount: 3, - views: 1247, - author: { _id: "u1", name: "Alice Dev", avatar: "/avatars/alice.jpg" }, - createdAt: new Date("2025-12-28"), - }, - // ... more questions -]; -``` - -### 4๏ธโƒฃ Update Homepage to Use Data Functions - -```typescript -// app/(root)/page.tsx -import { getAllQuestions } from "@/lib/data"; - -const HomePage = async () => { - const questions = await getAllQuestions(); - - return ( - <> -

All Questions

- {questions.map((question) => ( - - ))} - - ); -}; -``` - -### Does Database Choice Impact This? ๐Ÿค” - -**Short answer: No, not significantly.** - -The beauty of the Data Access Layer pattern is that your components don't care where data comes from. When you switch to a real database, you only change the implementation inside `lib/data/`: - -| Database | Change Required | -|----------|-----------------| -| MongoDB | Replace mock array with `db.collection('questions').find()` | -| Neon | Replace mock array with Prisma/Drizzle query | -| Convex | Replace mock array with `ctx.db.query("questions")` | - -Your components remain unchanged. This is called the **Repository Pattern**. - --- ## Database Options Compared @@ -174,15 +16,6 @@ Your components remain unchanged. This is called the **Repository Pattern**. | **Learning Curve** | Moderate | Low (if you know SQL) | Moderate (new paradigm) | | **AI-Friendly** | Good | Good | Excellent | -### Pricing Comparison ๐Ÿ’ฐ - -| Tier | MongoDB Atlas | Neon | Convex | -|------|---------------|------|--------| -| **Free** | 512 MB storage | 0.5 GB storage, 100 CU-hrs | 1M function calls, 0.5 GB | -| **Starter** | $8-30/mo (Flex) | $5/mo minimum | Pay-as-you-go | -| **Production** | ~$57/mo (M10) | ~$20-50/mo | $25/developer/mo | -| **Scale-to-Zero** | โŒ No | โœ… Yes | โœ… Yes | - ### Best Fit by Use Case ๐ŸŽฏ | If You Want... | Choose | @@ -478,14 +311,7 @@ When someone posts an answer, **all users viewing that question see it instantly --- -## Final Recommendation ๐ŸŽฏ - -### For Your Mock Data Now - -1. **Expand your types** to include tags on questions -2. **Create richer mock data** with realistic relationships -3. **Update the homepage** to use data functions (not hard-coded) -4. **Keep the Data Access Layer pattern** - it makes migration easy +## Recommendation ๐ŸŽฏ ### For Database Choice @@ -508,18 +334,3 @@ Based on your situation (learning project, Vercel deployment, using Clerk, build 5. **You already use Clerk** - Convex has documented Clerk integration **However**, if you prefer SQL and want the massive PostgreSQL ecosystem, **Neon is excellent** too. The PR branching feature is genuinely useful. - ---- - -## Next Steps ๐Ÿ“ - -1. โœ… Implement the Data Access Layer pattern (mock data abstraction) -2. โœ… Expand types to include question-tag relationships -3. โœ… Update homepage to use data functions -4. ๐Ÿ”œ Choose your database (I suggest trying Convex first) -5. ๐Ÿ”œ Replace mock data implementations with real database queries - ---- - -*Report generated for DevFlow Q&A Platform* -*Sources: Official documentation from MongoDB, Neon, Convex, and Vercel* From b2ca1e02e55d34eb148efba03706c00443692156 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sat, 3 Jan 2026 21:22:48 +0400 Subject: [PATCH 3/9] feat: add question cards to homepage with data layer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Homepage: - Replace hard-coded [1,2,3,4].map() with getAllQuestions() data fetching - Render QuestionCard components from mock data Components: - Add QuestionCard component displaying title, body excerpt, tags, author, stats - Link question titles to /question/:id route Data layer: - Expand Question type with body, tags, author, votes, answerCount, viewCount - Add getAllQuestions() and update getTopQuestions() to sort by votes - Populate 7 realistic mock questions with varied metadata Utilities: - Add getRelativeTime() for human-readable timestamps - Add unit tests covering minute/hour/day/week/month/year boundaries Establishes the data abstraction pattern for swapping mock data with a database. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/(app)/page.tsx | 81 ++++++------------------------ components/question-card.tsx | 63 ++++++++++++++++++++++++ lib/data/questions.ts | 95 +++++++++++++++++++++++++++++++++--- lib/utils.test.ts | 37 ++++++++++++++ lib/utils.ts | 38 +++++++++++++++ 5 files changed, 240 insertions(+), 74 deletions(-) create mode 100644 components/question-card.tsx create mode 100644 lib/utils.test.ts diff --git a/app/(app)/page.tsx b/app/(app)/page.tsx index 97ab7cd..f8d2224 100644 --- a/app/(app)/page.tsx +++ b/app/(app)/page.tsx @@ -1,73 +1,20 @@ -import { TagLink } from "@/components/tag-link"; +import { QuestionCard } from "@/components/question-card"; +import { getAllQuestions } from "@/lib/data/questions"; -const HomePage = () => ( - <> -

All Questions

+const HomePage = async () => { + const questions = await getAllQuestions(); -
- {[1, 2, 3, 4].map((questionIndex) => ( -
-
- - Question #{questionIndex} - - - Asked {questionIndex} hours ago - -
+ return ( + <> +

All Questions

-

- How to implement a sticky sidebar in Next.js with Tailwind CSS? -

- -

- I'm building a dashboard layout and need the sidebar to remain - visible while scrolling the main content. The sidebar should stick - below the navbar and scroll independently if its content overflows. - Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do - eiusmod tempor incididunt ut labore et dolore magna aliqua. -

- - {/* Tags */} -
- {["tailwind", "nextjs"].map((tag) => ( - - ))} -
- - {/* Stats */} -
- - - {questionIndex * 7} - {" "} - votes - - - - {questionIndex * 3} - {" "} - answers - - - - {questionIndex * 47} - {" "} - views - -
-
- ))} - - {/* End marker */} -
- You've reached the end โ€” sidebar should still be sticky! +
+ {questions.map((question) => ( + + ))}
-
- -); + + ); +}; export default HomePage; diff --git a/components/question-card.tsx b/components/question-card.tsx new file mode 100644 index 0000000..3e593d9 --- /dev/null +++ b/components/question-card.tsx @@ -0,0 +1,63 @@ +"use client"; + +import Link from "next/link"; +import { TagLink } from "@/components/tag-link"; +import type { Question } from "@/lib/data/questions"; +import { getRelativeTime } from "@/lib/utils"; + +type QuestionCardProps = { + question: Question; +}; + +export function QuestionCard({ question }: QuestionCardProps) { + return ( +
+
+ + {question.author.name} + + ยท + + Asked {getRelativeTime(question.createdAt)} + +
+ + +

+ {question.title} +

+ + +

{question.body}

+ + {/* Tags */} +
+ {question.tags.map((tag) => ( + + ))} +
+ + {/* Stats */} +
+ + + {question.votes} + {" "} + votes + + + + {question.answerCount} + {" "} + answers + + + + {question.viewCount} + {" "} + views + +
+
+ ); +} diff --git a/lib/data/questions.ts b/lib/data/questions.ts index fdc5c6e..76ca858 100644 --- a/lib/data/questions.ts +++ b/lib/data/questions.ts @@ -1,33 +1,114 @@ export type Question = { _id: string; title: string; + body: string; + tags: string[]; + author: { + _id: string; + name: string; + avatar?: string; + }; + votes: number; + answerCount: number; + viewCount: number; + createdAt: Date; }; const MOCK_QUESTIONS: Question[] = [ { _id: "1", + title: "How to centre a div?", + body: "I've been trying to centre a div both horizontally and vertically for hours. I've tried margin: auto, text-align: center, and various other approaches but nothing seems to work consistently. What's the most reliable modern approach?", + tags: ["css", "html"], + author: { _id: "u1", name: "Sarah Chen" }, + votes: 142, + answerCount: 12, + viewCount: 15420, + createdAt: new Date("2025-11-15"), + }, + { + _id: "2", title: "How to Ensure Unique User Profile with ON CONFLICT in PostgreSQL Using Drizzle ORM?", + body: "I'm building a user profile system where users can only have one profile. When they try to create a second profile, I want to update the existing one instead. How do I implement this upsert pattern with Drizzle ORM?", + tags: ["postgres", "nextjs"], + author: { _id: "u2", name: "Marcus Johnson" }, + votes: 89, + answerCount: 5, + viewCount: 3241, + createdAt: new Date("2025-12-20"), }, { - _id: "2", + _id: "3", title: "What are the benefits and trade-offs of using Server-Side Rendering (SSR) in Next.js?", + body: "I'm starting a new Next.js project and trying to decide between SSR, SSG, and client-side rendering. What are the real-world trade-offs I should consider? When does SSR actually make sense?", + tags: ["nextjs", "reactjs"], + author: { _id: "u3", name: "Emily Rodriguez" }, + votes: 67, + answerCount: 8, + viewCount: 4892, + createdAt: new Date("2025-12-18"), }, - { _id: "3", title: "How to centre a div?" }, { _id: "4", title: "Node.js res.json() and res.send(), not working but still able to change status code", + body: "I'm building an Express API and running into a strange issue. My res.json() and res.send() calls don't seem to send any response body, but the status code changes work fine. What could be causing this?", + tags: ["javascript", "nodejs"], + author: { _id: "u4", name: "Alex Kim" }, + votes: 45, + answerCount: 3, + viewCount: 1876, + createdAt: new Date("2025-12-22"), + }, + { + _id: "5", + title: "ReactJs or NextJs for beginners i ask for advice", + body: "I'm new to web development and want to learn React. Should I start with plain React or jump straight into Next.js? I've heard Next.js is more opinionated but provides better structure. What do experienced developers recommend?", + tags: ["reactjs", "nextjs"], + author: { _id: "u5", name: "Jordan Taylor" }, + votes: 38, + answerCount: 15, + viewCount: 6234, + createdAt: new Date("2025-12-25"), + }, + { + _id: "6", + title: "How to set up Tailwind CSS v4 with Next.js?", + body: "I'm trying to set up Tailwind CSS v4 in my Next.js project but the configuration seems different from v3. The @tailwind directives aren't working. What's the correct way to configure Tailwind v4 with the new @import syntax?", + tags: ["tailwind", "nextjs", "css"], + author: { _id: "u6", name: "Priya Patel" }, + votes: 23, + answerCount: 4, + viewCount: 1245, + createdAt: new Date("2025-12-28"), + }, + { + _id: "7", + title: "TypeScript generics explained with examples", + body: "I understand basic TypeScript but generics confuse me. Can someone explain with practical examples when and why I'd use generics? I've seen code like and have no idea what it means.", + tags: ["typescript", "javascript"], + author: { _id: "u7", name: "David Lee" }, + votes: 19, + answerCount: 6, + viewCount: 2103, + createdAt: new Date("2025-12-30"), }, - { _id: "5", title: "ReactJs or NextJs for beginners i ask for advice" }, ]; /** - * Fetch top questions sorted by votes/engagement. - * TODO: Replace with MongoDB query when database is set up. + * Fetch all questions for the homepage. + * TODO: Replace with database query when set up. + */ +export async function getAllQuestions(): Promise { + return MOCK_QUESTIONS; +} + +/** + * Fetch top questions sorted by votes. + * TODO: Replace with database query when set up. */ export async function getTopQuestions(limit = 5): Promise { - // Future: return await db.collection('questions').find().sort({ votes: -1 }).limit(limit) - return MOCK_QUESTIONS.slice(0, limit); + return [...MOCK_QUESTIONS].sort((a, b) => b.votes - a.votes).slice(0, limit); } diff --git a/lib/utils.test.ts b/lib/utils.test.ts new file mode 100644 index 0000000..c420e71 --- /dev/null +++ b/lib/utils.test.ts @@ -0,0 +1,37 @@ +import { getRelativeTime } from "@/lib/utils"; + +describe("getRelativeTime", () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date("2025-12-15T12:00:00Z")); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it.each([ + // Hours and minutes (finer granularity) + ["2025-12-15T11:59:30Z", "just now"], + ["2025-12-15T11:59:00Z", "1 minute ago"], + ["2025-12-15T11:55:00Z", "5 minutes ago"], + ["2025-12-15T11:00:00Z", "1 hour ago"], + ["2025-12-15T09:00:00Z", "3 hours ago"], + // Basic ranges + ["2025-12-15T08:00:00Z", "4 hours ago"], + ["2025-12-14T12:00:00Z", "yesterday"], + ["2025-12-12T12:00:00Z", "3 days ago"], + ["2025-12-01T12:00:00Z", "2 weeks ago"], + ["2025-09-15T12:00:00Z", "3 months ago"], + ["2023-12-15T12:00:00Z", "2 years ago"], + // Boundaries & singular forms + ["2025-12-09T12:00:00Z", "6 days ago"], + ["2025-12-08T12:00:00Z", "1 week ago"], + ["2025-11-16T12:00:00Z", "4 weeks ago"], + ["2025-11-15T12:00:00Z", "1 month ago"], + ["2024-12-16T12:00:00Z", "12 months ago"], + ["2024-12-15T12:00:00Z", "1 year ago"], + ])("returns '%s' โ†’ '%s'", (input, expected) => { + expect(getRelativeTime(new Date(input))).toBe(expected); + }); +}); diff --git a/lib/utils.ts b/lib/utils.ts index 8b03cf3..257e186 100644 --- a/lib/utils.ts +++ b/lib/utils.ts @@ -24,3 +24,41 @@ export const NAV_LINK_INACTIVE_CLASSES = "font-medium"; export function getNavIconInvertClasses(isActive: boolean): string { return cn(!isActive && "invert-colors", "shrink-0"); } + +/** + * Get a human-readable relative time string from a date. + * Granularity: just now โ†’ minutes โ†’ hours โ†’ yesterday โ†’ days โ†’ weeks โ†’ months โ†’ years + */ +export function getRelativeTime(date: Date): string { + const now = new Date(); + const diffInMs = now.getTime() - date.getTime(); + const diffInMinutes = Math.floor(diffInMs / (1000 * 60)); + const diffInHours = Math.floor(diffInMs / (1000 * 60 * 60)); + const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24)); + + // Minutes + if (diffInMinutes < 1) return "just now"; + if (diffInMinutes < 60) + return `${diffInMinutes} ${diffInMinutes === 1 ? "minute" : "minutes"} ago`; + + // Hours + if (diffInHours < 24) + return `${diffInHours} ${diffInHours === 1 ? "hour" : "hours"} ago`; + + // Days + if (diffInDays === 1) return "yesterday"; + if (diffInDays < 7) return `${diffInDays} days ago`; + + // Weeks + const weeks = Math.floor(diffInDays / 7); + if (diffInDays < 30) return `${weeks} ${weeks === 1 ? "week" : "weeks"} ago`; + + // Months + const months = Math.floor(diffInDays / 30); + if (diffInDays < 365) + return `${months} ${months === 1 ? "month" : "months"} ago`; + + // Years + const years = Math.floor(diffInDays / 365); + return `${years} ${years === 1 ? "year" : "years"} ago`; +} From 1b31034876bb3c30e33801336ba13cf7b8a7d1ae Mon Sep 17 00:00:00 2001 From: Michelle Date: Sun, 4 Jan 2026 03:37:03 +0400 Subject: [PATCH 4/9] refactor: relocate theme toggle and adjust main content spacing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout: - Reduce top padding from 48px to 24px to align with right sidebar - Increase horizontal padding on smaller screens (24px โ†’ 40px) Navigation: - Move ThemeToggle from desktop top bar to left sidebar footer - Add GlobalSearch placeholder component in top bar Aligns page headings with right sidebar content. Theme toggle is now grouped with other persistent controls in the sidebar footer, keeping the top bar cleaner. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- app/(app)/layout.tsx | 2 +- components/navigation/desktop-topbar.tsx | 7 ++++--- components/navigation/left-sidebar.tsx | 11 +++++++---- components/search/global-search.tsx | 7 +++++++ 4 files changed, 19 insertions(+), 8 deletions(-) create mode 100644 components/search/global-search.tsx diff --git a/app/(app)/layout.tsx b/app/(app)/layout.tsx index cbbdfd3..2b51a02 100644 --- a/app/(app)/layout.tsx +++ b/app/(app)/layout.tsx @@ -21,7 +21,7 @@ const RootLayout = async ({ children }: { children: React.ReactNode }) => { {/* Content + Right Sidebar row */}
-
+
{children}
diff --git a/components/navigation/desktop-topbar.tsx b/components/navigation/desktop-topbar.tsx index 069a12e..a468efb 100644 --- a/components/navigation/desktop-topbar.tsx +++ b/components/navigation/desktop-topbar.tsx @@ -1,5 +1,5 @@ import { SignedOut, SignInButton, SignUpButton } from "@clerk/nextjs"; -import { ThemeToggle } from "@/components/navigation/theme-toggle"; +import { GlobalSearch } from "@/components/search/global-search"; import { Button } from "@/components/ui/button"; export function DesktopTopBar() { @@ -8,13 +8,14 @@ export function DesktopTopBar() { {/* Left section: matches main content structure (padding + max-w-5xl centering) */}
-

Global Search

+
+ +
{/* Right section: matches right sidebar width on xl */}
- diff --git a/components/navigation/left-sidebar.tsx b/components/navigation/left-sidebar.tsx index c89428f..fb16c27 100644 --- a/components/navigation/left-sidebar.tsx +++ b/components/navigation/left-sidebar.tsx @@ -6,6 +6,7 @@ import Link from "next/link"; import { usePathname } from "next/navigation"; import { LeftSidebarToggle } from "@/components/navigation/left-sidebar-toggle"; import { NAV_LINKS } from "@/components/navigation/nav-links.constants"; +import { ThemeToggle } from "@/components/navigation/theme-toggle"; import { ThemedFullLogo } from "@/components/navigation/themed-full-logo"; import { Sidebar, @@ -95,14 +96,16 @@ export function LeftSidebar() { - {/* Footer: UserButton + Toggle */} - + {/* Footer: ThemeToggle + UserButton + Toggle */} + +
Global Search

; +} From ac761630d80c4c4d543a4c38b6fdc4b4c70210c0 Mon Sep 17 00:00:00 2001 From: Michelle Date: Sun, 4 Jan 2026 12:16:08 +0400 Subject: [PATCH 5/9] fix: align theme toggle with user avatar in sidebar Navigation: - Reduce ThemeToggle button from 40px to 32px to match Clerk UserButton size - Update placeholder element to match new button dimensions Fixes horizontal misalignment between the theme toggle icon and user avatar when the left sidebar is expanded. Both elements now share the same 32px width. --- components/navigation/theme-toggle.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/components/navigation/theme-toggle.tsx b/components/navigation/theme-toggle.tsx index 3901ec6..0bf2ffc 100644 --- a/components/navigation/theme-toggle.tsx +++ b/components/navigation/theme-toggle.tsx @@ -12,13 +12,13 @@ export function ThemeToggle() { useEffect(() => setMounted(true), []); if (!mounted) { - return