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 ( +
+

Rate a Meal

+
+ + +
+
+ + +
+
+ +