Skip to content
Closed
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
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# added by me
*.db

# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
Expand Down
63 changes: 63 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# AI Agent Guide for `nextjs-workshop`

This document provides context and guidelines for AI agents working on this codebase.

## Project Overview
This represents a "Build Your First Full-Stack App in Next.js" workshop demo. It is a simple Full-Stack application allowing users to view and submit ratings.

## Tech Stack & versions
- **Next.js:** 16.1.6 (App Router)
- **React:** 19.2.3
- **TypeScript:** v5
- **Tailwind CSS:** v4 (configured via `postcss.config.mjs` and imported in `app/globals.css`)
- **Database:** SQLite (LibSQL client)
- **ORM:** Drizzle ORM v0.45.1

## Architecture Patterns

### App Router
- The project uses the Next.js **App Router** (`app/` directory).
- `page.tsx` is the main entry point.
- `layout.tsx` handles the global shell (HTML/Body structure).

### Data Fetching
- Data fetching should be done directly in Server Components using `drizzle-orm`.
- **Example:** `await db.select().from(ratings)` inside `app/page.tsx`.

### Mutations (Server Actions)
- Mutations are handled via **Server Actions**.
- Located in `actions/index.ts`.
- Components invoke these actions via `form` `action` prop or event handlers.
- **Note:** Ensure `revalidatePath` is called after mutations to update the UI.

### Database
- **Configuration:** `db/index.ts` sets up the LibSQL client and Drizzle instance.
- **Schema:** `db/schema.ts` defines the tables.
- **Table `ratings`**:
- `id`: int (primary key, auto increment)
- `name`: text (not null)
- `rating`: int (not null)
- `comment`: text (optional)

### Styling
- Tailwind CSS v4 is used.
- Class names are used directly on elements.
- Global styles are in `app/globals.css`.

## Common Tasks

### Adding a new Database Field
1. Edit `db/schema.ts` to add the field.
2. Run `npx drizzle-kit push` (or generate migration) to update the DB.
3. Update UI components to display/input the new field.

### Adding a new Feature
1. Define data requirements in `db/schema.ts`.
2. Create/Update Server Actions in `actions/index.ts`.
3. Create UI in `components/` or a new page in `app/`.

## Key Files
- `drizzle.config.ts`: Configuration for Drizzle Kit.
- `next.config.ts`: Next.js configuration.
- `actions/index.ts`: Business logic & DB writes.
- `db/schema.ts`: Database definition.
110 changes: 107 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,114 @@ Presentation and supporting materials for my workshop "Build Your First Full-Sta

Created and presented by Jeremie Bornais

## 📚 Slides & Materials

The presentation slides and guides are available in the `docs` directory:

- [View Slides (Markdown)](docs/slides.md)
- [View Slides (PDF)](docs/slides.pdf)
- [Complete Build Guide](docs/full_instructions.md) - Step-by-step instructions from project init to fully working app

## 🛠 Tech Stack

This project uses the modern Next.js stack:

- **Framework:** [Next.js 16](https://nextjs.org/) (App Router)
- **Language:** [TypeScript](https://www.typescriptlang.org/)
- **Styling:** [Tailwind CSS v4](https://tailwindcss.com/)
- **Database:** [SQLite](https://www.sqlite.org/) (via [LibSQL](https://docs.turso.tech/sdk/ts/quickstart))
- **ORM:** [Drizzle ORM](https://orm.drizzle.team/)
- **UI Library:** [React 19](https://react.dev/)

## 🚀 Getting Started

1. **Install dependencies:**

```bash
npm install
```

2. **Environment Setup:**

Copy `example.env` to `.env`:

```bash
cp example.env .env
```

3. **Run Development Server:**

```bash
npm run dev
```

Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.

## 📂 Project Structure

```text
├── actions/ # Server Actions (mutations)
├── app/ # Next.js App Router pages and layouts
├── components/ # React UI components
├── db/ # Database configuration and schema
├── docs/ # Workshop slides and documentation
└── public/ # Static assets
```

## 🌍 Deployment

### 1. Database (Turso)

Since this project uses LibSQL, the easiest way to deploy the database is with [Turso](https://turso.tech/).

1. **Install Turso CLI**: Follow the [installation instructions](https://docs.turso.tech/cli/installation).

2. **Create Database**:
```bash
turso db create nextjs-workshop
```

3. **Get Connection Details**:
Get the Database URL:
```bash
turso db show nextjs-workshop --url
```
Get the Auth Token:
```bash
turso db tokens create nextjs-workshop
```

4. **Push Schema to Turso**:
Now that you have your credentials, you need to push your database schema to the new Turso database.

Update your local `.env` file with the Turso URL and Token temporarily, then run the Drizzle migration:

```bash
npx drizzle-kit push
```

### 2. Application (Vercel)

The easiest way to deploy your Next.js app is with [Vercel](https://vercel.com/new).

1. **Push to GitHub**: Push your code to a GitHub repository.

2. **Import Project**: Go to Vercel, click "Add New...", and select "Project". Import your GitHub repository.

3. **Environment Variables**:
In the Vercel project configuration, add the following Environment Variables:

| Variable | Value |
| :--- | :--- |
| `DB_URL` | The **Database URL** from step 3 (starts with `libsql://`) |
| `DB_AUTH_TOKEN` | The **Auth Token** from step 3 |

4. **Deploy**: Click "Deploy". Your app should be live in a minute!

## Learn More

To learn more about Next.js, take a look at the following resources:
To learn more about the tools used in this workshop:

- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.

- [Drizzle ORM Docs](https://orm.drizzle.team/docs/overview) - type-safe SQL ORM.
- [Tailwind CSS Docs](https://tailwindcss.com/docs) - utility-first CSS framework.
25 changes: 25 additions & 0 deletions actions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"use server";

import { db } from "@/db";
import { ratings } from "@/db/schema";
import { eq } from "drizzle-orm";
import { revalidatePath } from "next/cache";

export async function addRating(formData: FormData) {
const name = formData.get("name") as string;
const rating = Number(formData.get("rating"));
const comment = formData.get("comment") as string;

await db.insert(ratings).values({
name,
rating,
comment,
});

revalidatePath("/");
}

export async function deleteRating(id: number) {
await db.delete(ratings).where(eq(ratings.id, id));
revalidatePath("/");
}
77 changes: 77 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
@@ -1 +1,78 @@
@import "tailwindcss";

@layer base {
body {
@apply bg-gradient-to-b from-orange-50 to-amber-50 text-gray-800;
}

main {
@apply max-w-2xl mx-auto p-6 sm:p-8;
}

h1 {
@apply text-4xl sm:text-5xl font-bold text-orange-600 mb-8 text-center;
}

h2 {
@apply text-2xl font-semibold text-orange-700 mb-6 mt-0;
}

h3 {
@apply text-xl font-semibold text-gray-900 mb-2;
}

form {
@apply bg-white rounded-xl shadow-md p-6 mb-12 border border-orange-100;
}

form div {
@apply mb-4;
}

label {
@apply block text-sm font-semibold text-gray-700 mb-2;
}

input,
select,
textarea {
@apply w-full px-4 py-2 rounded-lg border border-gray-300 focus:outline-none focus:ring-2 focus:ring-orange-500 focus:border-transparent bg-white text-gray-900 transition-all;
}

textarea {
@apply resize min-h-24;
}

button {
@apply px-6 py-2 rounded-lg font-semibold transition-all;
}

form button {
@apply w-full bg-gradient-to-r from-orange-500 to-amber-500 text-white hover:from-orange-600 hover:to-amber-600 active:scale-95 shadow-md;
}

button:not(form button) {
@apply bg-red-500 text-white hover:bg-red-600 active:scale-95 text-sm;
}

/* Rating cards container */
main > div:last-child {
@apply bg-white rounded-xl shadow-md p-6 border border-orange-100;
}

main > div:last-child > div {
@apply mb-6 p-4 border-l-4 border-orange-400 bg-orange-50 rounded hover:shadow-md transition-shadow;
}

main > div:last-child > div:last-child {
@apply mb-0;
}

main > div:last-child p {
@apply text-gray-600 mb-4 italic;
}

main > div:last-child button {
@apply mt-2;
}
}
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ import type { Metadata } from "next";
import "./globals.css";

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Campus Cravings",
description: "Rate your favorite meals on campus!",
};

export default function RootLayout({
Expand Down
37 changes: 30 additions & 7 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,30 @@
export default function Home() {
return (
<main>
<div>Hello world!</div>
</main>
);
}
import { db } from "@/db";
import { ratings } from "@/db/schema";
import { desc } from "drizzle-orm";
import RatingForm from "@/components/RatingForm";
import DeleteButton from "@/components/DeleteButton";

export default async function Home() {
const allRatings = await db.select().from(ratings).orderBy(desc(ratings.id));

return (
<main>
<h1>Campus Cravings 🍔</h1>
<RatingForm />
<div>
<h2>Recent Reviews</h2>

{allRatings.length === 0 && <p>No ratings yet. Be the first!</p>}

{allRatings.map((rating) => (
<div key={rating.id}>
<h3>{rating.name}</h3>
<div>{rating.rating}/5</div>
<p>{rating.comment}</p>
<DeleteButton id={rating.id} />
</div>
))}
</div>
</main>
);
}
9 changes: 9 additions & 0 deletions components/DeleteButton.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
"use client";

import { deleteRating } from "@/actions";

export default function DeleteButton({ id }: { id: number }) {
return (
<button onClick={() => deleteRating(id)}>Delete</button>
);
}
30 changes: 30 additions & 0 deletions components/RatingForm.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
"use client";

import { addRating } from "@/actions";

export default function RatingForm() {
return (
<form action={addRating}>
<h2>Rate a Meal</h2>
<div>
<label>Meal Name</label>
<input name="name" placeholder="Spicy Tofu" required />
</div>
<div>
<label>Rating</label>
<select name="rating">
{[5, 4, 3, 2, 1].map((num) => (
<option key={num} value={num}>
{num} Stars
</option>
))}
</select>
</div>
<div>
<label>Comments</label>
<textarea name="comment" placeholder="It was great..." />
</div>
<button type="submit">Submit Rating</button>
</form>
);
}
Loading