AirDish

A modern restaurant & food website template with a built-in admin dashboard, online reservations, dynamic menu, blog, and image management.

Next.js 16 React 19 TypeScript Tailwind CSS 4 PostgreSQL Admin Panel Included

Thank you for purchasing AirDish. This guide walks you through everything you need to install, configure, and launch your new restaurant website โ€” written so that anyone can follow it, even if this is your first time working with a Next.js project. Please read the steps in order.

Quick Start (TL;DR)

If you are already comfortable with Node.js and PostgreSQL, here is the entire setup in five commands. Detailed explanations follow below.

# 1. Install dependencies
npm install

# 2. Create your .env file from the template, then fill in DATABASE_URL,
#    DIRECT_URL, ADMIN_PASSWORD, EMAIL_ENCRYPTION_KEY (and S3 keys if you want uploads)
cp .env.example .env

# 3. Generate Prisma client + create all database tables
npx prisma generate
npx prisma migrate deploy

# 4. (Optional) Load demo content so the site doesn't look empty
npm run seed

# 5. Start the development server
npm run dev

Open http://localhost:3000 for the site and http://localhost:3000/admin to sign in.

Need a key for EMAIL_ENCRYPTION_KEY? Generate one with openssl rand -base64 32 and paste the output into your .env. It is required for the email subsystem (reservation confirmation emails).

1. Introduction

AirDish is a complete, production-ready website for restaurants, cafรฉs, and food businesses. Unlike a static HTML template, AirDish is a full-stack application โ€” it comes with a password-protected admin dashboard where you can manage your menu, categories, blog posts, reservations, FAQs, and homepage content without touching any code.

Visitors can browse your menu, read your blog, subscribe to your newsletter, and book a table online. Every booking and subscription is stored safely in your database and is instantly visible in the admin dashboard.

In short: AirDish is a beautiful restaurant website plus a content management system, all in one package.

2. Features

๐Ÿ  Modern Landing Page

Animated single-page design: Hero, About, Menu, Categories, Testimonials, Reservation, FAQ, and Blog sections.

๐Ÿ” Admin Dashboard

Secure, password-protected control panel at /admin to manage all site content.

๐Ÿ“… Online Reservations

Guests book a specific table for a date and time window, optionally pre-ordering menu items. Confirm/cancel and export to CSV.

๐Ÿช‘ Table Management

Define your real-world tables (name + seats). The booking form checks live availability per table and time window.

๐Ÿฝ๏ธ Dynamic Menu & Categories

Add, edit, reorder, and hide menu items and food categories at any time.

โญ Customer Testimonials

Visitors leave reviews on menu items; you approve, reject, reorder, and feature them on the homepage.

โœ‰๏ธ Reservation Emails

Automatic confirmation emails via Mailtrap (Resend/SendGrid/SES/SMTP ready), with an idempotent audit log of every send.

โœ๏ธ Blog with Rich Editor

Write posts with a built-in rich-text editor, tags, cover images, and SEO-friendly URLs.

๐Ÿ“ง Newsletter Capture

Collect subscriber emails from the site and view them in the dashboard.

๐Ÿ–ผ๏ธ Cloud Image Uploads

Upload images directly to Amazon S3 (or compatible) storage from the admin panel.

๐Ÿ“ฑ Fully Responsive

Looks great on desktops, tablets, and phones out of the box.

3. Technology Stack

AirDish is built with modern, well-supported technologies:

Technology Purpose
Next.js 16 (App Router) The core framework that powers the website and its server features.
React 19 Builds the interactive user interface.
TypeScript Adds type safety for reliable, maintainable code.
Tailwind CSS 4 Utility-first styling for the entire site.
Ant Design 6 UI components used in the admin dashboard.
Framer Motion Smooth animations and transitions.
Tiptap The rich-text editor for writing blog posts.
Prisma 7 + PostgreSQL Database layer that stores all your content, reservations, and email logs.
Zod Runtime validation for environment variables and API payloads.
AWS S3 (or any S3-compatible service) Cloud storage for uploaded images.
Mailtrap (default email provider) Sends reservation confirmation emails. Schema also supports Resend, SendGrid, SES, and generic SMTP.

4. Requirements

Before you begin, make sure you have the following ready:

  • Node.js 20 or newer โ€” download from nodejs.org. (This project was built and tested on Node v20.19.3.)
  • A PostgreSQL database โ€” a free Neon account works perfectly and is recommended. Any PostgreSQL provider (Supabase, Railway, AWS RDS, or a local install) also works.
  • An Amazon S3 bucket (or S3-compatible storage like Cloudflare R2, DigitalOcean Spaces, or MinIO) โ€” required only if you want to upload images through the admin dashboard.
  • A code editor such as Visual Studio Code (optional, but helpful).
  • Basic comfort using a terminal / command line.
Check your Node version by running node -v in your terminal. If it prints a number lower than 20, please update Node.js first.

5. Folder Structure

Here is an overview of the most important files and folders so you know where everything lives:

airdish/
โ”œโ”€โ”€ prisma/
โ”‚   โ”œโ”€โ”€ schema.prisma          # Database structure (tables & fields)
โ”‚   โ”œโ”€โ”€ migrations/            # Database version history
โ”‚   โ””โ”€โ”€ seed.ts                # Optional sample-data script (run: npm run seed)
โ”œโ”€โ”€ public/
โ”‚   โ”œโ”€โ”€ images/                # Default demo images
โ”‚   โ”œโ”€โ”€ themes/airdish/        # Brand logo & favicon
โ”‚   โ””โ”€โ”€ *.svg                  # Icons
โ”œโ”€โ”€ scripts/
โ”‚   โ””โ”€โ”€ migrate-sqlite-to-neon.ts # One-off helper (legacy data migration)
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ app/
โ”‚   โ”‚   โ”œโ”€โ”€ page.tsx           # The homepage
โ”‚   โ”‚   โ”œโ”€โ”€ layout.tsx         # Site-wide layout, fonts & theme color
โ”‚   โ”‚   โ”œโ”€โ”€ globals.css        # Global styles
โ”‚   โ”‚   โ”œโ”€โ”€ admin/            # The admin dashboard (/admin)
โ”‚   โ”‚   โ”œโ”€โ”€ blog/             # Blog post pages (/blog/...)
โ”‚   โ”‚   โ””โ”€โ”€ api/              # Backend endpoints (menu, reservations, tables, ...)
โ”‚   โ”œโ”€โ”€ components/          # Homepage sections (Hero, Menu, Footer...)
โ”‚   โ”œโ”€โ”€ lib/                 # DB client, env validation, crypto, defaults
โ”‚   โ”œโ”€โ”€ server/              # Backend services (email, reservations)
โ”‚   โ””โ”€โ”€ generated/           # Auto-generated Prisma client (do not edit)
โ”œโ”€โ”€ .env                       # YOUR private settings (you create this)
โ”œโ”€โ”€ .env.example               # Template for the .env file (local / Neon)
โ”œโ”€โ”€ .env.production.example    # Template for self-hosted production deploys
โ”œโ”€โ”€ next.config.ts             # Next.js configuration (allowed image domains)
โ”œโ”€โ”€ prisma.config.ts           # Prisma CLI configuration
โ”œโ”€โ”€ package.json               # Dependencies & scripts
โ””โ”€โ”€ docs/                      # This documentation
You will mostly only edit one file: the .env file. Everything else can be managed from the admin dashboard once the site is running.

6. Step-by-Step Setup

Follow these steps in order. Open a terminal inside the project folder (the folder that contains package.json) before you start.

  1. Install the dependencies

    This downloads all the libraries the project needs. It may take a couple of minutes.

    npm install
  2. Create your environment file

    Copy the provided template to a new file named .env, then open it in your editor. You will fill in the values in the next section.

    # macOS / Linux
    cp .env.example .env
    
    # Windows (Command Prompt)
    copy .env.example .env
  3. Fill in your settings

    Edit the .env file with your database and storage details. See Environment Variables below for what each value means.

  4. Generate the database client

    This prepares Prisma to talk to your database.

    npx prisma generate
  5. Create the database tables

    This builds all the tables your site needs inside your PostgreSQL database.

    npx prisma migrate deploy
  6. (Optional) Load sample data

    Skip this if you want to start completely empty. Otherwise, populate menu items, categories, blog posts, FAQs, tables, and demo testimonials so you can immediately see how the site looks with content:

    npm run seed
  7. Start the website

    Launch the development server and open the site in your browser.

    npm run dev

    Now visit http://localhost:3000 to see your site, and http://localhost:3000/admin to log in to the dashboard.

That's it! Your site is now running locally. Sign in to the admin dashboard with your ADMIN_PASSWORD and start replacing the demo content with your own.

7. Environment Variables

The .env file holds all your private settings. Never share this file or commit it publicly โ€” it contains your passwords and keys. Below is the full template with safe placeholder values:

# ---- Database (PostgreSQL / Neon) ----
DATABASE_URL="postgresql://USER:[email protected]/DBNAME?sslmode=require&pgbouncer=true&connect_timeout=15"
DIRECT_URL="postgresql://USER:[email protected]/DBNAME?sslmode=require"

# ---- Admin dashboard password ----
ADMIN_PASSWORD="choose-a-strong-password"

# ---- Email subsystem (required to send reservation confirmations) ----
# Generate with:  openssl rand -base64 32
EMAIL_ENCRYPTION_KEY="your-32-byte-base64-key"
# Optional fallback sender used until you save EmailSettings in /admin โ†’ Settings
EMAIL_FALLBACK_SENDER_EMAIL="[email protected]"
EMAIL_FALLBACK_SENDER_NAME="Your Restaurant"
# Brand bits embedded in the email template
RESTAURANT_NAME="Your Restaurant"
RESTAURANT_SUPPORT_EMAIL="[email protected]"

# ---- Image storage (Amazon S3 or compatible) ----
AWS_ACCESS_KEY_ID="your-access-key"
AWS_SECRET_ACCESS_KEY="your-secret-key"
AWS_REGION="your-region"
AWS_BUCKET="your-bucket-name"
NEXT_PUBLIC_S3BUCKET="https://your-cdn-or-bucket-domain"
Variable What it is Required?
DATABASE_URL The pooled connection string your website uses to read/write data. From Neon, use the pooled connection. Yes
DIRECT_URL A direct (non-pooled) connection string used only when creating/changing database tables. From Neon, use the direct connection. (Can be the same value as DATABASE_URL for non-pooled setups.) Yes
ADMIN_PASSWORD The password you'll type to log in to /admin. Choose something strong and unique. Yes
EMAIL_ENCRYPTION_KEY Base64-encoded 32-byte key used to encrypt the email provider API token at rest. Generate once with openssl rand -base64 32 and keep it constant โ€” rotating it invalidates saved tokens. For email
EMAIL_FALLBACK_SENDER_EMAIL "From" address used if the admin has not yet saved a sender in /admin โ†’ Settings. Optional
EMAIL_FALLBACK_SENDER_NAME "From" name to match the fallback email. Optional
RESTAURANT_NAME Restaurant name shown inside reservation emails. Defaults to "AirDish". Optional
RESTAURANT_SUPPORT_EMAIL Support address shown in email footers. Optional
AWS_ACCESS_KEY_ID Access key for your S3 storage account. For uploads
AWS_SECRET_ACCESS_KEY Secret key for your S3 storage account. For uploads
AWS_REGION The region your bucket lives in, e.g. ap-southeast-1. For uploads
AWS_BUCKET The name of your storage bucket. For uploads
NEXT_PUBLIC_S3BUCKET The public base URL/endpoint where uploaded images are served from. For uploads
Security: Always use your own real credentials here and keep them private. The values above are placeholders only. Replace ADMIN_PASSWORD with a strong password and generate a brand-new EMAIL_ENCRYPTION_KEY before going live โ€” never reuse the demo values.
No S3 yet? You can still run the whole site and use every feature except uploading new images. The site ships with demo images and the admin panel also accepts image URLs you paste in manually.
No email yet? The site works without it โ€” reservations are still saved and visible in the admin dashboard. You only need the email variables once you want guests to receive automatic confirmation messages.

8. Database Setup

AirDish needs a PostgreSQL database. You have two common options โ€” pick whichever fits your hosting plan:

Option A โ€” Hosted database (Neon, recommended for most buyers)

Neon is a free, managed PostgreSQL service that takes about two minutes to set up. Supabase, Railway, AWS RDS, and Vercel Postgres all work the same way.

  1. Create a database

    Sign up at neon.tech, click New Project, pick the closest region, and accept the defaults. Neon will create a database for you.

  2. Copy the two connection strings

    On the Neon dashboard click Connection Details. Copy the Pooled connection string into DATABASE_URL, and the Direct connection string into DIRECT_URL in your .env file. Keep the ?sslmode=require suffix.

  3. Create the tables

    Run the migration. This builds every table the site needs:

    npx prisma migrate deploy

Option B โ€” Self-hosted PostgreSQL (VPS / dedicated server)

If you are deploying to your own Linux server (Ubuntu, Debian, etc.), you can install PostgreSQL locally and skip the hosted provider.

  1. Install PostgreSQL

    # Ubuntu / Debian
    sudo apt update
    sudo apt install -y postgresql postgresql-contrib
  2. Create a database and user

    sudo -u postgres psql
    
    -- Inside psql:
    CREATE DATABASE airdish;
    CREATE USER airdish_user WITH ENCRYPTED PASSWORD 'CHOOSE-A-STRONG-PASSWORD';
    GRANT ALL PRIVILEGES ON DATABASE airdish TO airdish_user;
    \q
  3. Set both URLs to the same local connection

    In .env (or .env.production.local on the server), use:

    DATABASE_URL="postgresql://airdish_user:[email protected]:5432/airdish?sslmode=disable"
    DIRECT_URL="postgresql://airdish_user:[email protected]:5432/airdish?sslmode=disable"
    Use 127.0.0.1, not a LAN IP like 192.168.x.x. The production host can't reach a private LAN address โ€” every DB query will return 500.
  4. Create the tables

    npx prisma migrate deploy

Viewing your data (optional)

Prisma includes a friendly visual database browser. Run this and open the URL it prints:

npx prisma studio

Loading demo content (optional)

If you want the homepage to look populated immediately, run the included seed script. It inserts demo menu items, categories, blog posts, FAQs, tables, and testimonials โ€” safe to run multiple times.

npm run seed
Your site starts with an empty database. That's expected. Skip seeding and the homepage shows sensible default content until you add your own from the admin dashboard.

9. Running the Project

The project includes a few simple commands (defined in package.json):

Command What it does
npm run dev Starts the development server at http://localhost:3000. Use this while building/previewing your site.
npm run build Runs prisma generate then builds an optimized production bundle. Run before deploying.
npm start Runs the production build locally (run npm run build first).
npm run seed Loads demo content (menu, categories, blog, FAQs, tables, testimonials). Idempotent โ€” safe to re-run.
npm run lint Checks the code for common issues.
npm run type-check Runs TypeScript without emitting files, to catch type errors.
npx prisma studio Opens a visual database browser in your web browser.
npx prisma migrate deploy Applies any pending database migrations. Run after pulling updates or on first install.
Day-to-day: use npm run dev. When you're ready to publish, use npm run build followed by npm start (or deploy to a host โ€” see Deployment).

10. Admin Dashboard

The admin dashboard is the heart of AirDish. Visit /admin (for example http://localhost:3000/admin) and log in using the ADMIN_PASSWORD you set in your .env file.

What you can manage

Section What you can do
Reservations View all table bookings (with the chosen table, time window, and any pre-ordered dishes). Confirm or cancel them โ€” a confirmation email is sent automatically when you confirm, if email is configured. Export everything to CSV.
Menu Items Add dishes with name, price, original price, and photo. Reorder, hide, or delete them.
Categories Manage food categories shown on the homepage.
Tables Define your real tables (name + seat count). The public reservation form uses this to check live availability per table and time window, so two parties can't book the same table at once.
Blog Posts Write and publish articles using the rich-text editor, with cover image, author, tags, and read time.
FAQs Add and reorder frequently asked questions.
Testimonials Moderate customer reviews: approve, reject, reorder, feature on the homepage, and edit. Each review is tied to a specific menu item.
Newsletter View everyone who subscribed to your newsletter.
Hero Content Edit the big headline, description, and main image at the top of the homepage.
About Content Edit the "About" section text, images, and statistics.
Settings Configure the email provider used to send reservation confirmations (see Email setup below). Send a test email to verify the configuration.
Important: Anyone with the admin password can edit your site. Keep it secret, use a strong password, and change it (in .env) if you ever suspect it has been exposed.

11. Email (Reservation Confirmations)

When you confirm a reservation in the admin dashboard, AirDish sends the guest an automatic confirmation email containing the date, time, table, and pre-ordered items (if any). Email is optional โ€” the site works fine without it โ€” but enabling it is a nice touch for your guests.

Step 1 โ€” Set the encryption key (one-time)

The email provider's API token is stored in your database encrypted at rest, so a database leak alone cannot expose it. You must provide the encryption key in your .env file:

# Generate this once, then keep it constant across deploys
openssl rand -base64 32

Copy the output and paste it as:

EMAIL_ENCRYPTION_KEY="paste-the-output-here"
Do not change this key after saving an email provider token. Rotating the key invalidates the stored token and you'll need to re-save it in the admin Settings.

Step 2 โ€” Pick an email provider

The bundled provider is Mailtrap (free tier available). The schema also supports Resend, SendGrid, SES, and generic SMTP โ€” each requires a small provider class to be added to src/server/email/providers/. For most ThemeForest buyers, Mailtrap is the fastest path.

  1. Create a Mailtrap account

    Sign up at mailtrap.io. For real customer emails use a Sending Domain (Email API โ†’ Sending Domains), not the sandbox inbox.

  2. Copy your API token

    Mailtrap โ†’ Sending Domains โ†’ API and SMTP โ†’ copy the API Token.

  3. Save the settings in the admin dashboard

    Sign in to /admin, go to Settings, and fill in:

    • Provider: Mailtrap
    • API Token: paste the token from Mailtrap
    • Sender Email: an address on your verified Mailtrap sending domain (e.g. [email protected])
    • Sender Name: your restaurant name as guests should see it

    Click Save, then Send Test Email to verify everything works.

How sending works

  • Emails go out automatically when an admin marks a reservation as Confirmed.
  • Every send attempt is recorded in the EmailLog table (status: PENDING / SENT / FAILED / SKIPPED) so you have a full audit trail.
  • Each attempt uses a unique idempotency key โ€” clicking "Confirm" twice will not send two emails.
  • If a reservation was submitted without an email address, the system records SKIPPED and does not send.
Just want to test locally? Mailtrap's free Sandbox inbox catches every email so nothing accidentally goes to a real customer. Switch to a Sending Domain later when you're ready for production.

12. Image Uploads (S3)

When you upload an image in the admin dashboard, AirDish stores it in your S3 bucket and saves the public link. To enable this, fill in the five AWS_* / NEXT_PUBLIC_S3BUCKET values in your .env file.

Upload rules

  • Allowed formats: JPEG, PNG, WebP, GIF, and AVIF.
  • Maximum size: 5 MB per image.

Displaying images from other domains

For security, Next.js only displays optimized images from approved domains. If your images are hosted somewhere new, add that domain to next.config.ts:

const nextConfig = {
  images: {
    remotePatterns: [
      { protocol: "https", hostname: "images.unsplash.com" },
      { protocol: "https", hostname: "ds.manob.ai" },
      // Add your own image/CDN domain here:
      { protocol: "https", hostname: "your-cdn-or-bucket-domain" },
    ],
  },
};
Compatible with more than AWS. Any S3-compatible service (Cloudflare R2, DigitalOcean Spaces, MinIO, etc.) works โ€” just point NEXT_PUBLIC_S3BUCKET at its endpoint and use that provider's keys.

13. Customization

Brand / accent color

The site's signature orange is set in one place. Open src/app/layout.tsx and change colorPrimary:

theme={{
  token: {
    colorPrimary: "#f97316",  // โ† change to your brand color
    borderRadius: 8,
  },
}}

Site title & description (SEO)

Edit the metadata object in src/app/layout.tsx:

export const metadata = {
  title: "Your Restaurant Name โ€” A Culinary Journey",
  description: "Your own description here",
};

Homepage content

Most homepage content (Hero, About, Menu, Categories, Blog, FAQ) is edited from the admin dashboard โ€” no coding required. To rearrange or remove whole sections, edit src/app/page.tsx, where each section is a clearly named component.

Fonts

AirDish uses Inter and Playfair Display from Google Fonts, configured at the top of src/app/layout.tsx. Swap them for any Google Font there.

Demo images

Default placeholder images live in public/images/. Replace these files (keeping the same names) to change demo visuals, or upload your own through the admin dashboard.

14. Deployment

AirDish is a standard Next.js app and can be hosted anywhere that supports Node.js. Vercel (made by the creators of Next.js) is the simplest option.

Deploying to Vercel

  1. Push your project to a Git provider

    Upload the project to GitHub, GitLab, or Bitbucket.

  2. Import it into Vercel

    In Vercel, click "New Project" and select your repository.

  3. Add your environment variables

    Copy every value from your .env file into Vercel's Environment Variables settings.

  4. Deploy

    Vercel runs npm run build automatically and publishes your live site.

Deploying to your own server (VPS / dedicated)

The project ships with a separate template, .env.production.example, for deploys where Postgres runs on the same machine as Node. On the server:

# 1. Pull / upload the project, then:
npm install --omit=dev
cp .env.production.example .env.production.local
# Edit .env.production.local โ€” use 127.0.0.1 for the DB host, NOT a 192.168.* IP

# 2. Set up the database (if you haven't already โ€” see Option B above)
npx prisma migrate deploy

# 3. Build & run
npm run build
npm start

For long-running deploys, run npm start under a process manager such as PM2 or a systemd service, and put Nginx in front for HTTPS.

Before going live, double-check:
  • A strong ADMIN_PASSWORD.
  • A freshly generated EMAIL_ENCRYPTION_KEY (don't reuse the dev key).
  • Correct production database URLs (use 127.0.0.1 for local Postgres, not a LAN IP).
  • Real S3 credentials and your image domain listed in next.config.ts.
  • Email provider saved in /admin โ†’ Settings and a successful test email.

15. Troubleshooting

"DATABASE_URL is not set"

Your .env file is missing or the DATABASE_URL line is empty. Make sure the file is named exactly .env (not .env.txt), is in the project root, and contains a valid connection string. Restart the server after editing.

Can't connect to the database / timeout

Check that the connection string is correct and that your database allows connections. For Neon, confirm you used the pooled URL for DATABASE_URL and the direct URL for DIRECT_URL.

"Table does not exist" errors

You haven't created the tables yet. Run npx prisma migrate deploy.

Admin login fails

The password you typed must match ADMIN_PASSWORD in .env exactly. After changing it, restart the server.

Image upload fails

Verify all five S3 settings are filled in correctly, the file is under 5 MB, and it's one of the allowed formats (JPEG, PNG, WebP, GIF, AVIF).

An uploaded image won't display

Add the image's domain to remotePatterns in next.config.ts (see Image Uploads), then restart.

Changes to .env aren't taking effect

Environment changes only load when the server starts. Stop the server (Ctrl + C) and run npm run dev again.

"Email encryption key is not configured"

You haven't set EMAIL_ENCRYPTION_KEY in .env. Generate one with openssl rand -base64 32, paste it in, then restart the server. See Email setup.

"EMAIL_ENCRYPTION_KEY must decode to exactly 32 bytes"

The value you pasted isn't valid base64 or isn't 32 bytes. Re-generate it with openssl rand -base64 32 and paste the entire output, including any trailing =.

Test email fails / "Failed to send"

Open /admin โ†’ Settings and re-check the provider token, sender email, and that the sender address is on a verified Mailtrap sending domain. The error message returned to the admin UI is sanitised โ€” full provider errors are written to the server log.

Reservation form says "No tables available"

Go to /admin โ†’ Tables and add at least one table with a seat count. The reservation form filters tables by guest count and time window โ€” if every table is already booked for that window, none will be selectable.

Production site returns 500 on every page

Almost always a database connection problem. On a self-hosted production server, make sure DATABASE_URL uses 127.0.0.1 (or your DB hostname), not a 192.168.x.x LAN IP from your development network โ€” the production host cannot reach it.

16. Sources & Credits

This template was built using these excellent open-source projects and resources:

Demo photos are placeholders for preview purposes. Please replace them with your own licensed images before publishing your live website.

17. Support

If you run into a problem this guide doesn't cover:

  1. Re-read the Troubleshooting section โ€” most issues are listed there.
  2. Confirm your .env values are correct and the server was restarted after changes.
  3. Make sure you completed every step in Step-by-Step Setup, in order.
  4. Reach out through your ThemeForest item's Support tab with a clear description of the issue and any error message you see.

Thank you again for choosing AirDish. We hope it serves your business beautifully. ๐Ÿฝ๏ธ