diff --git a/.gitignore b/.gitignore
index 5ef6a52..f2bf1be 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,6 @@
+# added by me
+*.db
+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..0b550cc
--- /dev/null
+++ b/AGENTS.md
@@ -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.
diff --git a/README.md b/README.md
index 0532663..88b1052 100644
--- a/README.md
+++ b/README.md
@@ -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.
diff --git a/actions/index.ts b/actions/index.ts
new file mode 100644
index 0000000..5c10dd4
--- /dev/null
+++ b/actions/index.ts
@@ -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("/");
+}
\ No newline at end of file
diff --git a/app/globals.css b/app/globals.css
index f1d8c73..f0c918f 100644
--- a/app/globals.css
+++ b/app/globals.css
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/app/layout.tsx b/app/layout.tsx
index 756fcce..8840b67 100644
--- a/app/layout.tsx
+++ b/app/layout.tsx
@@ -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({
diff --git a/app/page.tsx b/app/page.tsx
index b95c3ed..e0cc644 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -1,7 +1,30 @@
-export default function Home() {
- return (
-
-
Hello world!
-
- );
-}
+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 (
+
+
Campus Cravings 🍔
+
+
+
Recent Reviews
+
+ {allRatings.length === 0 &&
No ratings yet. Be the first!
}
+
+ {allRatings.map((rating) => (
+
+
{rating.name}
+
{rating.rating}/5
+
{rating.comment}
+
+
+ ))}
+
+
+ );
+}
\ No newline at end of file
diff --git a/components/DeleteButton.tsx b/components/DeleteButton.tsx
new file mode 100644
index 0000000..045ae06
--- /dev/null
+++ b/components/DeleteButton.tsx
@@ -0,0 +1,9 @@
+"use client";
+
+import { deleteRating } from "@/actions";
+
+export default function DeleteButton({ id }: { id: number }) {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/components/RatingForm.tsx b/components/RatingForm.tsx
new file mode 100644
index 0000000..a37d0dc
--- /dev/null
+++ b/components/RatingForm.tsx
@@ -0,0 +1,30 @@
+"use client";
+
+import { addRating } from "@/actions";
+
+export default function RatingForm() {
+ return (
+
+ );
+}
\ No newline at end of file
diff --git a/db/index.ts b/db/index.ts
new file mode 100644
index 0000000..e2189b7
--- /dev/null
+++ b/db/index.ts
@@ -0,0 +1,12 @@
+import { config } from "dotenv";
+import { drizzle } from "drizzle-orm/libsql";
+import { createClient } from "@libsql/client";
+
+config({ path: ".env" });
+
+const client = createClient({
+ url: process.env.DB_URL!,
+ authToken: process.env.DB_AUTH_TOKEN,
+});
+
+export const db = drizzle(client);
diff --git a/db/schema.ts b/db/schema.ts
new file mode 100644
index 0000000..5d28c05
--- /dev/null
+++ b/db/schema.ts
@@ -0,0 +1,8 @@
+import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
+
+export const ratings = sqliteTable("ratings", {
+ id: int("id").primaryKey({ autoIncrement: true }),
+ name: text("name").notNull(),
+ rating: int("rating").notNull(),
+ comment: text("comment"),
+});
\ No newline at end of file
diff --git a/docs/.nojekyll b/docs/.nojekyll
new file mode 100644
index 0000000..e69de29
diff --git a/docs/full_instructions.md b/docs/full_instructions.md
new file mode 100644
index 0000000..583737b
--- /dev/null
+++ b/docs/full_instructions.md
@@ -0,0 +1,395 @@
+# Campus Cravings: Complete Build Guide
+
+Build a full-stack meal rating app with Next.js, React, Drizzle ORM, and SQLite. This guide takes you from zero to a fully working application.
+
+## Prerequisites
+
+- A modern version of Node.js and npm installed
+
+## Step 1: Initialize the Project
+
+Create a new Next.js project with the minimal template:
+
+```bash
+npx create-next-app@latest --empty --yes campus-cravings
+cd campus-cravings
+npm run dev
+```
+
+This creates a fresh Next.js 16 project with the App Router. You should see the dev server running on `http://localhost:3000`.
+
+## Step 2: Install Dependencies
+
+Install the required packages for database management, authentication tokens, and environment variables:
+
+```bash
+npm install drizzle-orm @libsql/client dotenv
+npm install -D drizzle-kit
+```
+
+**What these do:**
+- `drizzle-orm` - TypeScript ORM for database queries
+- `@libsql/client` - SQLite client with full-stack support
+- `dotenv` - Load environment variables from `.env`
+- `drizzle-kit` - CLI tool for schema management and migrations
+
+## Step 3: Configure Environment Variables
+
+Create a `.env` file in the root directory with your database URL and authentication token:
+
+```bash
+DB_URL=file:local.db
+DB_AUTH_TOKEN=supersecrettoken
+```
+
+**Note:** The `DB_URL=file:local.db` creates a local SQLite database file. In production, you'd use a remote database URL. The `DB_AUTH_TOKEN` is required by the LibSQL client for authentication, even for local files. You can use any string for local development.
+
+## Step 4: Define the Database Schema
+
+Create `db/schema.ts` to define your data structure. This describes the `ratings` table:
+
+```ts
+import { int, sqliteTable, text } from "drizzle-orm/sqlite-core";
+
+export const ratings = sqliteTable("ratings", {
+ id: int("id").primaryKey({ autoIncrement: true }),
+ name: text("name").notNull(),
+ rating: int("rating").notNull(),
+ comment: text("comment"),
+});
+```
+
+**Schema breakdown:**
+- `id` - Auto-incrementing primary key
+- `name` - Meal name (required)
+- `rating` - Rating out of 5 (required)
+- `comment` - Optional user comment
+
+## Step 5: Initialize the Database Client
+
+Create `db/index.ts` to set up the Drizzle ORM instance and LibSQL client:
+
+```ts
+import { config } from "dotenv";
+import { drizzle } from "drizzle-orm/libsql";
+import { createClient } from "@libsql/client";
+
+config({ path: ".env" });
+
+const client = createClient({
+ url: process.env.DB_URL!,
+ authToken: process.env.DB_AUTH_TOKEN!,
+});
+
+export const db = drizzle(client);
+```
+
+This file exports the `db` instance which you'll use throughout your app to query the database.
+
+## Step 6: Configure Drizzle Kit
+
+Create `drizzle.config.ts` for schema management and migrations:
+
+```ts
+import { config } from "dotenv";
+import { defineConfig } from "drizzle-kit";
+
+config({ path: ".env" });
+
+export default defineConfig({
+ schema: "./db/schema.ts",
+ out: "./migrations",
+ dialect: "turso",
+ dbCredentials: {
+ url: process.env.DB_URL!,
+ authToken: process.env.DB_AUTH_TOKEN!,
+ },
+});
+```
+
+## Step 7: Create the Database
+
+Push your schema to create the database file and tables:
+
+```bash
+npx drizzle-kit push
+```
+
+**What happens:** This command creates the `local.db` file and the `ratings` table based on your schema.
+
+## Step 8: Style with Tailwind CSS v4
+
+Update `app/globals.css` with Tailwind styles for your app:
+
+```css
+@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;
+ }
+}
+```
+
+## Step 9: Create Server Actions
+
+Create `actions/index.ts` to handle form submissions and deletions on the server:
+
+```ts
+"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("/");
+}
+```
+
+**How it works:**
+- `addRating()` - Inserts a new rating into the database and revalidates the page
+- `deleteRating()` - Removes a rating by ID and revalidates the page
+- `revalidatePath()` - Tells Next.js to refresh the page cache after mutations
+
+## Step 10: Create the Rating Form Component
+
+Create `components/RatingForm.tsx` - a Client Component for the form:
+
+```tsx
+"use client";
+
+import { addRating } from "@/actions";
+
+export default function RatingForm() {
+ return (
+
+ );
+}
+```
+
+**Note:** The `"use client"` directive makes this a Client Component. The `action={addRating}` prop connects the form to the server action.
+
+## Step 11: Create the Delete Button Component
+
+Create `components/DeleteButton.tsx` - a reusable Delete button:
+
+```tsx
+"use client";
+
+import { deleteRating } from "@/actions";
+
+export default function DeleteButton({ id }: { id: number }) {
+ return (
+
+ );
+}
+```
+
+## Step 12: Build the Home Page
+
+Create `app/page.tsx` - the main Server Component that fetches and displays all ratings:
+
+```tsx
+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 (
+
+
Campus Cravings 🍔
+
+
+
Recent Reviews
+
+ {allRatings.length === 0 &&
No ratings yet. Be the first!
}
+
+ {allRatings.map((rating) => (
+
+
{rating.name}
+
{rating.rating}/5
+
{rating.comment}
+
+
+ ))}
+
+
+ );
+}
+```
+
+**Key features:**
+- `async` function for server-side data fetching
+- `orderBy(desc(ratings.id))` shows newest ratings first
+- Empty state message if no ratings exist
+- Dynamic rendering of rating cards with delete buttons
+
+## Running the App
+
+Start the development server:
+
+```bash
+npm run dev
+```
+
+Visit `http://localhost:3000` and you should see:
+- A form to submit meal ratings
+- A section displaying all previously submitted ratings
+- Delete buttons to remove ratings
+
+## Project Structure
+
+```
+.
+├── app/
+│ ├── globals.css # Tailwind styles
+│ ├── layout.tsx # Root layout (auto-generated)
+│ └── page.tsx # Home page
+├── components/
+│ ├── RatingForm.tsx # Form for new ratings
+│ └── DeleteButton.tsx # Delete action button
+├── db/
+│ ├── index.ts # Drizzle client
+│ └── schema.ts # Database schema
+├── actions/
+│ └── index.ts # Server actions
+├── .env # Environment variables
+├── drizzle.config.ts # Drizzle configuration
+└── package.json # Dependencies
+```
+
+## Next Steps
+
+Once you have the basic app working, you can extend it with:
+- **Search/Filter** - Add filtering by rating or meal name
+- **Edit functionality** - Allow users to update existing ratings
+- **User authentication** - Track who submitted each rating
+- **Analytics** - Show average ratings, popular meals, etc.
+- **Database backups** - Set up schema migrations with Drizzle Kit
+
+## Troubleshooting
+
+**Database file not created:**
+- Ensure `.env` exists in the root directory
+- Run `npx drizzle-kit push` again
+
+**Form submissions not working:**
+- Check that `DB_URL` and `DB_AUTH_TOKEN` are set in `.env`
+- Ensure the `ratings` table exists in the database
+
+**Styles not applying:**
+- Verify `app/globals.css` is imported in your layout
+- Tailwind CSS v4 is configured via `postcss.config.mjs`
diff --git a/docs/index.html b/docs/index.html
new file mode 100644
index 0000000..4e9883e
--- /dev/null
+++ b/docs/index.html
@@ -0,0 +1,196 @@
+
+
+
+
+
+ Intro to Next.js Workshop
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+