Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 6 additions & 0 deletions fullstack/fullstack/.dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules
.next
.git
Dockerfile
.dockerignore
.env.local
41 changes: 41 additions & 0 deletions fullstack/fullstack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.pnpm-debug.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts
23 changes: 23 additions & 0 deletions fullstack/fullstack/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Use official Node.js LTS image
FROM node:20-alpine

# Set working directory
WORKDIR /app

# Copy package.json and package-lock.json (or yarn.lock)
COPY package*.json ./

# Install dependencies
RUN npm install

# Copy the rest of the project
COPY . .

# Build the Next.js app
RUN npm run build

# Expose the default Next.js port
EXPOSE 3000

# Start the Next.js app
CMD ["npm", "start"]
102 changes: 102 additions & 0 deletions fullstack/fullstack/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
## Fullstack Assessment — Next.js Blog (1–2 Years)

### Overview

A full-featured blog application built with **Next.js 14 App Router**, **Tailwind CSS**, and **Docker**. The app covers all required assessment criteria plus several enhancements for production readiness.

---

### ✅ Requirements Completed

| Requirement | Status | Notes |
|---|---|---|
| Blog post list page | ✓ | Featured post hero + responsive card grid |
| Single post page | ✓ | Full content with prev/next navigation |
| `GET /api/posts` | ✓ | Returns all posts (summary, no full content) |
| `GET /api/posts/:id` | ✓ | Returns single post; 400 on bad ID, 404 on missing |
| Tailwind CSS styling | ✓ | Dark theme, responsive, custom scrollbar |

---

### 🗂 Project Structure

```
fullstack/
├── app/
│ ├── layout.js # Root layout — sticky nav, footer
│ ├── page.js # Home: hero, featured post, post grid, API panel
│ ├── globals.css # Tailwind directives + prose component styles
│ ├── posts/
│ │ └── [id]/
│ │ └── page.js # Dynamic single post page
│ └── api/
│ ├── posts/
│ │ └── route.js # GET /api/posts
│ └── posts/[id]/
│ └── route.js # GET /api/posts/:id
├── data/
│ └── posts.js # Static blog data (4 posts)
├── Dockerfile # Multi-stage production build
├── docker-compose.yml # One-command local run
├── next.config.js # output: standalone (required for Docker)
├── tailwind.config.js
└── postcss.config.js
```

---

### 🎨 Design Decisions

- **Featured post** — First post gets a full-width hero card; remaining posts render in a responsive 1→2→3 column grid.
- **Tag color system** — Each post tag (Framework, Styling, Data, API) has its own color badge, making posts visually scannable at a glance.
- **Prev / Next navigation** — Bottom of each post page links to adjacent posts so readers can browse without going back to the list.
- **Responsive at every breakpoint** — Mobile-first with `sm:` and `lg:` breakpoints. Nav collapses, grid stacks, code blocks scroll horizontally.
- **Custom mini markdown renderer** — Parses `**bold**`, `` `inline code` ``, and ` ``` ` fenced code blocks with language labels directly in JSX — no external library needed.

---

### 🐳 Docker Setup

The app runs in a **3-stage Docker build** to keep the production image small and secure:

1. **`deps`** — installs npm dependencies
2. **`builder`** — runs `next build` with `output: standalone`
3. **`runner`** — copies only the standalone output; runs as a non-root user

**Run with Docker Compose:**
```bash
docker compose up --build
```
Visit `http://localhost:3000`

**Or build and run manually:**
```bash
docker build -t devnotes .
docker run -p 3000:3000 devnotes
```
---

### 🔌 API Reference

| Method | Endpoint | Response |
|---|---|---|
| `GET` | `/api/posts` | `{ count: 4, posts: [...] }` — all posts without `content` |
| `GET` | `/api/posts/:id` | Full post object including `content` |
| `GET` | `/api/posts/999` | `404 { error: "Post with id 999 not found." }` |
| `GET` | `/api/posts/abc` | `400 { error: "Invalid ID — must be a number." }` |

---

### 🚀 Local Development

```bash
cd fullstack
npm install
npm run dev
# → http://localhost:3000
```

---

### Screenshots

27 changes: 27 additions & 0 deletions fullstack/fullstack/app/api/post/[id]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { getPostById } from '@/data/posts'
import { NextResponse } from 'next/server'

interface RouteProps {
params: Promise<{
id: string
}>
}

export async function GET(
request: Request,
props: RouteProps
) {
const params = await props.params
const id = Number(params.id)

const post = getPostById(id)

if (!post) {
return NextResponse.json(
{ error: 'Post not found' },
{ status: 404 }
)
}

return NextResponse.json(post)
}
6 changes: 6 additions & 0 deletions fullstack/fullstack/app/api/post/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { posts } from '@/data/posts'
import { NextResponse } from 'next/server'

export async function GET() {
return NextResponse.json(posts)
}
Binary file added fullstack/fullstack/app/favicon.ico
Binary file not shown.
125 changes: 125 additions & 0 deletions fullstack/fullstack/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
@import 'tailwindcss';


@custom-variant dark (&:is(.dark *));

:root {
--background: oklch(1 0 0);
--foreground: oklch(0.2 0 0);
--card: oklch(1 0 0);
--card-foreground: oklch(0.2 0 0);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.2 0 0);
--primary: oklch(0.65 0.23 142);
--primary-foreground: oklch(1 0 0);
--secondary: oklch(1 0 0);
--secondary-foreground: oklch(0.2 0 0);
--muted: oklch(0.95 0 0);
--muted-foreground: oklch(0.55 0 0);
--accent: oklch(0.65 0.23 142);
--accent-foreground: oklch(1 0 0);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.577 0.245 27.325);
--border: oklch(0.92 0 0);
--input: oklch(0.92 0 0);
--ring: oklch(0.65 0.23 142);
--chart-1: oklch(0.646 0.222 41.116);
--chart-2: oklch(0.6 0.118 184.704);
--chart-3: oklch(0.398 0.07 227.392);
--chart-4: oklch(0.828 0.189 84.429);
--chart-5: oklch(0.769 0.188 70.08);
--radius: 0.625rem;
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.145 0 0);
--sidebar-primary: oklch(0.205 0 0);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.97 0 0);
--sidebar-accent-foreground: oklch(0.205 0 0);
--sidebar-border: oklch(0.922 0 0);
--sidebar-ring: oklch(0.708 0 0);
}

.dark {
--background: oklch(0.145 0 0);
--foreground: oklch(0.985 0 0);
--card: oklch(0.145 0 0);
--card-foreground: oklch(0.985 0 0);
--popover: oklch(0.145 0 0);
--popover-foreground: oklch(0.985 0 0);
--primary: oklch(0.985 0 0);
--primary-foreground: oklch(0.205 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
--muted-foreground: oklch(0.708 0 0);
--accent: oklch(0.269 0 0);
--accent-foreground: oklch(0.985 0 0);
--destructive: oklch(0.396 0.141 25.723);
--destructive-foreground: oklch(0.637 0.237 25.331);
--border: oklch(0.269 0 0);
--input: oklch(0.269 0 0);
--ring: oklch(0.439 0 0);
--chart-1: oklch(0.488 0.243 264.376);
--chart-2: oklch(0.696 0.17 162.48);
--chart-3: oklch(0.769 0.188 70.08);
--chart-4: oklch(0.627 0.265 303.9);
--chart-5: oklch(0.645 0.246 16.439);
--sidebar: oklch(0.205 0 0);
--sidebar-foreground: oklch(0.985 0 0);
--sidebar-primary: oklch(0.488 0.243 264.376);
--sidebar-primary-foreground: oklch(0.985 0 0);
--sidebar-accent: oklch(0.269 0 0);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(0.269 0 0);
--sidebar-ring: oklch(0.439 0 0);
}

@theme inline {
--font-sans: 'Geist', 'Geist Fallback';
--font-mono: 'Geist Mono', 'Geist Mono Fallback';
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
}

@layer base {
* {
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
}
}
43 changes: 43 additions & 0 deletions fullstack/fullstack/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import type { Metadata } from 'next'
import { Geist, Geist_Mono } from 'next/font/google'
import './globals.css'

const _geist = Geist({ subsets: ["latin"] });
const _geistMono = Geist_Mono({ subsets: ["latin"] });

export const metadata: Metadata = {
title: 'My Blog',
description: 'Exploring web development, design, and technology',
generator: 'v0.app',
icons: {
icon: [
{
url: '/icon-light-32x32.png',
media: '(prefers-color-scheme: light)',
},
{
url: '/icon-dark-32x32.png',
media: '(prefers-color-scheme: dark)',
},
{
url: '/icon.svg',
type: 'image/svg+xml',
},
],
apple: '/apple-icon.png',
},
}

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode
}>) {
return (
<html lang="en">
<body className="font-sans antialiased">
{children}
</body>
</html>
)
}
Loading