Skip to content
Merged
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
43 changes: 14 additions & 29 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,21 @@
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
# AppThrust Next.js PostgreSQL template

## Getting Started
This starter proves the AppThrust managed PostgreSQL path:

First, run the development server:
- AppThrust injects `DATABASE_URL` through `ComponentConnection`.
- The initial schema is applied through `DatabaseChange`.
- The Next.js app reads and writes `appthrust_demo_messages`.

The app does not run migrations on startup. For local development, apply
`db/migrations/0001_init.sql` to your PostgreSQL database, then set:

```bash
npm run dev
# or
yarn dev
# or
pnpm dev
# or
bun dev
DATABASE_URL=postgresql://app:password@localhost:5432/app
```

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

You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.

This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.

## Learn More

To learn more about Next.js, take a look at the following resources:

- [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.
Run locally:

You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!

## Deploy on Vercel

The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.

Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
```bash
npm install
npm run dev
```
14 changes: 14 additions & 0 deletions app/actions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
"use server";

import { revalidatePath } from "next/cache";
import { insertDemoMessage } from "@/lib/db";

export async function createMessage(formData: FormData) {
const body = String(formData.get("body") ?? "").trim();
if (!body) {
return;
}

await insertDemoMessage(body);
revalidatePath("/");
}
4 changes: 2 additions & 2 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "AppThrust PostgreSQL sample",
description: "A database-backed starter application for AppThrust",
};

export default function RootLayout({
Expand Down
221 changes: 162 additions & 59 deletions app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,65 +1,168 @@
import Image from "next/image";
import { createMessage } from "./actions";
import { loadDemoMessages } from "@/lib/db";

export const dynamic = "force-dynamic";

export default async function Home() {
const database = await loadDemoMessages();

export default function Home() {
return (
<div className="flex flex-col flex-1 items-center justify-center bg-zinc-50 font-sans dark:bg-black">
<main className="flex flex-1 w-full max-w-3xl flex-col items-center justify-between py-32 px-16 bg-white dark:bg-black sm:items-start">
<Image
className="dark:invert"
src="/next.svg"
alt="Next.js logo"
width={100}
height={20}
priority
/>
<div className="flex flex-col items-center gap-6 text-center sm:items-start sm:text-left">
<h1 className="max-w-xs text-3xl font-semibold leading-10 tracking-tight text-black dark:text-zinc-50">
To get started, edit the page.tsx file.
</h1>
<p className="max-w-md text-lg leading-8 text-zinc-600 dark:text-zinc-400">
Looking for a starting point or more instructions? Head over to{" "}
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
>
Templates
</a>{" "}
or the{" "}
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="font-medium text-zinc-950 dark:text-zinc-50"
<main className="min-h-screen bg-slate-50 px-6 py-8 text-slate-950">
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6">
<header className="flex flex-col gap-4 border-b border-slate-200 pb-6 md:flex-row md:items-end md:justify-between">
<div>
<p className="text-sm font-medium text-emerald-700">
AppThrust sample
</p>
<h1 className="mt-2 text-3xl font-semibold tracking-normal">
Managed PostgreSQL messages
</h1>
</div>
<StatusBadge status={database.status} />
</header>

<section className="grid gap-4 md:grid-cols-3">
<Metric label="Connection" value={databaseStatusLabel(database.status)} />
<Metric label="Database" value={database.databaseName ?? "unavailable"} />
<Metric label="Messages" value={String(database.messages.length)} />
</section>

<section className="grid gap-6 lg:grid-cols-[minmax(0,1fr)_320px]">
<div className="rounded-lg border border-slate-200 bg-white">
<div className="border-b border-slate-200 px-5 py-4">
<h2 className="text-base font-medium">Recent messages</h2>
</div>
<div className="divide-y divide-slate-100">
{database.messages.length > 0 ? (
database.messages.map((message) => (
<article key={message.id} className="px-5 py-4">
<p className="text-sm text-slate-950">{message.body}</p>
<p className="mt-1 font-mono text-xs text-slate-500">
#{message.id} · {message.createdAt}
</p>
</article>
))
) : (
<div className="px-5 py-10 text-sm text-slate-500">
{emptyStateMessage(database.status)}
</div>
)}
</div>
</div>

<aside className="space-y-4">
<form
action={createMessage}
className="rounded-lg border border-slate-200 bg-white p-4"
>
Learning
</a>{" "}
center.
</p>
</div>
<div className="flex flex-col gap-4 text-base font-medium sm:flex-row">
<a
className="flex h-12 w-full items-center justify-center gap-2 rounded-full bg-foreground px-5 text-background transition-colors hover:bg-[#383838] dark:hover:bg-[#ccc] md:w-[158px]"
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
<Image
className="dark:invert"
src="/vercel.svg"
alt="Vercel logomark"
width={16}
height={16}
/>
Deploy Now
</a>
<a
className="flex h-12 w-full items-center justify-center rounded-full border border-solid border-black/[.08] px-5 transition-colors hover:border-transparent hover:bg-black/[.04] dark:border-white/[.145] dark:hover:bg-[#1a1a1a] md:w-[158px]"
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Documentation
</a>
</div>
</main>
<label
htmlFor="body"
className="text-sm font-medium text-slate-900"
>
New message
</label>
<textarea
id="body"
name="body"
rows={4}
maxLength={280}
placeholder="Write a database-backed message"
className="mt-2 w-full resize-none rounded-md border border-slate-300 px-3 py-2 text-sm outline-none focus:border-emerald-600 focus:ring-2 focus:ring-emerald-100"
/>
<button
type="submit"
disabled={database.status !== "ready"}
className="mt-3 w-full rounded-md bg-emerald-700 px-3 py-2 text-sm font-medium text-white disabled:cursor-not-allowed disabled:bg-slate-300"
>
Save message
</button>
</form>

<div className="rounded-lg border border-slate-200 bg-white p-4">
<h2 className="text-sm font-medium">Runtime</h2>
<dl className="mt-3 space-y-2 text-sm">
<InfoRow
label="DATABASE_URL"
value={database.databaseUrlPresent ? "provided" : "missing"}
/>
<InfoRow label="Host" value={database.host ?? "unavailable"} />
<InfoRow
label="Migration"
value={
database.status === "migration-pending"
? "pending"
: database.status === "ready"
? "applied"
: "unknown"
}
/>
</dl>
</div>
</aside>
</section>
</div>
</main>
);
}

function StatusBadge({ status }: { status: string }) {
const ready = status === "ready";

return (
<span
className={`inline-flex w-fit items-center rounded-full border px-3 py-1 text-sm font-medium ${
ready
? "border-emerald-200 bg-emerald-50 text-emerald-800"
: "border-amber-200 bg-amber-50 text-amber-800"
}`}
>
{databaseStatusLabel(status)}
</span>
);
}

function Metric({ label, value }: { label: string; value: string }) {
return (
<div className="rounded-lg border border-slate-200 bg-white p-4">
<p className="text-xs font-medium uppercase text-slate-500">{label}</p>
<p className="mt-2 truncate text-lg font-semibold">{value}</p>
</div>
);
}

function InfoRow({ label, value }: { label: string; value: string }) {
return (
<div className="flex items-center justify-between gap-3">
<dt className="text-slate-500">{label}</dt>
<dd className="min-w-0 truncate font-mono text-xs text-slate-900">
{value}
</dd>
</div>
);
}

function databaseStatusLabel(status: string) {
switch (status) {
case "ready":
return "Connected";
case "migration-pending":
return "Migration pending";
case "missing-url":
return "Not connected";
default:
return "Unavailable";
}
}

function emptyStateMessage(status: string) {
switch (status) {
case "migration-pending":
return "The database is reachable, but the initial schema has not been applied yet.";
case "missing-url":
return "DATABASE_URL is not configured for this runtime.";
case "unavailable":
return "The database could not be reached from this runtime.";
default:
return "No messages have been saved yet.";
}
}
9 changes: 9 additions & 0 deletions db/migrations/0001_init.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS appthrust_demo_messages (
id BIGSERIAL PRIMARY KEY,
body TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

INSERT INTO appthrust_demo_messages (body)
SELECT 'Hello from AppThrust managed PostgreSQL'
WHERE NOT EXISTS (SELECT 1 FROM appthrust_demo_messages);
Loading
Loading