diff --git a/components/Page.tsx b/components/Page.tsx index c41d713..2db69fc 100644 --- a/components/Page.tsx +++ b/components/Page.tsx @@ -1,32 +1,25 @@ import { JSX } from "preact"; -const NAV_ITEMS = { - "/note": "Notes", - "/register": "Register", - "/login": "Login", -}; - -const navItem = ([url, text]: [string, string]) => { - return ( - {text} - ); -}; +const NAV_ITEM_CLASSES = + "flex justify-center items-center px-4 py-2 text-blue-500 hover:bg-purple-700"; export function Page(props: JSX.HTMLAttributes) { return ( - - - - LyricScreen - - - {Object.entries(NAV_ITEMS).map(navItem)} + + + + + LyricScreen + + Notes + Register + Login - + {props.children} - diff --git a/db-migrations.ts b/db-migrations.ts index ac39c29..e9d9e8a 100644 --- a/db-migrations.ts +++ b/db-migrations.ts @@ -1,13 +1,39 @@ import { query } from "./db.ts"; -await query(` - create extension if not exists "uuid-ossp"; +const id = "id uuid primary key default uuid_generate_v4()"; - drop table if exists notes; - create table if not exists notes ( - id uuid primary key default uuid_generate_v4(), - content text not null, - created_at timestamptz not null default now(), - updated_at timestamptz not null default now() +const timestamps = [ + "created_at timestamptz not null default now()", + "updated_at timestamptz not null default now()", +]; + +const tables = { + "note": { + columns: [id, "content text not null", ...timestamps], + }, + "user": { + columns: [ + id, + "username text not null unique", + "hashed_password text not null", + ...timestamps, + ], + constraints: [], + }, +}; + +const tableStatements = Object.entries(tables).map(([name, meta]) => ` + drop table if exists "${name}"; + create table "${name}" ( + ${meta.columns.join(",\n ")} ); `); + +console.log(tableStatements); + +const queryString = ` + create extension if not exists "uuid-ossp"; + ${tableStatements.map((s) => s.trim()).join("\n\n ")} +`; +console.log(queryString); +await query(queryString); diff --git a/db.ts b/db.ts index 2262e82..349ec70 100644 --- a/db.ts +++ b/db.ts @@ -1,18 +1,28 @@ -import * as postgres from "https://deno.land/x/postgres@v0.16.1/mod.ts"; -import { type QueryArguments } from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments"; +import { + Pool, + PostgresError, +} from "https://deno.land/x/postgres@v0.16.1/mod.ts"; +import { + type QueryArguments, + type QueryObjectResult, +} from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments"; -export { type QueryObjectResult } from "https://deno.land/x/postgres@v0.16.1/query/query.ts?s=QueryArguments"; +export { PostgresError }; +export { type QueryObjectResult }; const databaseUrl = Deno.env.get("DATABASE_URL") || "postgresql://danielflanagan:@127.0.0.1:5432/lyricscreen"; -const pool = new postgres.Pool(databaseUrl, 3, true); +const pool = new Pool(databaseUrl, 3, true); -export async function query(sql: string, ...args: QueryArguments[]) { +export async function query( + sql: string, + ...args: QueryArguments[] +): Promise | null> { let result = null; try { const connection = await pool.connect(); try { - result = await connection.queryObject(sql, ...args); + result = connection.queryObject(sql, ...args); } catch (err) { console.error("Error querying database:", err); } finally { diff --git a/fresh.gen.ts b/fresh.gen.ts index 7d0e6fd..4d6bd63 100644 --- a/fresh.gen.ts +++ b/fresh.gen.ts @@ -13,10 +13,13 @@ import * as $6 from "./routes/api/random-uuid.ts"; import * as $7 from "./routes/countdown.tsx"; import * as $8 from "./routes/github/[username].tsx"; import * as $9 from "./routes/index.tsx"; -import * as $10 from "./routes/note.tsx"; -import * as $11 from "./routes/register.tsx"; -import * as $12 from "./routes/route-config-example.tsx"; -import * as $13 from "./routes/search.tsx"; +import * as $10 from "./routes/login.tsx"; +import * as $11 from "./routes/note.tsx"; +import * as $12 from "./routes/note/[id].tsx"; +import * as $13 from "./routes/note/create.tsx"; +import * as $14 from "./routes/register.tsx"; +import * as $15 from "./routes/route-config-example.tsx"; +import * as $16 from "./routes/search.tsx"; import * as $$0 from "./islands/Countdown.tsx"; import * as $$1 from "./islands/Counter.tsx"; @@ -32,10 +35,13 @@ const manifest = { "./routes/countdown.tsx": $7, "./routes/github/[username].tsx": $8, "./routes/index.tsx": $9, - "./routes/note.tsx": $10, - "./routes/register.tsx": $11, - "./routes/route-config-example.tsx": $12, - "./routes/search.tsx": $13, + "./routes/login.tsx": $10, + "./routes/note.tsx": $11, + "./routes/note/[id].tsx": $12, + "./routes/note/create.tsx": $13, + "./routes/register.tsx": $14, + "./routes/route-config-example.tsx": $15, + "./routes/search.tsx": $16, }, islands: { "./islands/Countdown.tsx": $$0, diff --git a/routes/login.tsx b/routes/login.tsx new file mode 100644 index 0000000..a2203fa --- /dev/null +++ b/routes/login.tsx @@ -0,0 +1,15 @@ +import { Page } from "../components/Page.tsx"; + +export default function Login() { + return ( + + + Username + + Password + + + + + ); +} diff --git a/routes/note.tsx b/routes/note.tsx index 9a1f03f..2ba7c77 100644 --- a/routes/note.tsx +++ b/routes/note.tsx @@ -1,5 +1,6 @@ import { Handlers, PageProps } from "$fresh/server.ts"; import { query } from "../db.ts"; +import { Page } from "../components/Page.tsx"; interface Note { id: string; @@ -7,10 +8,10 @@ interface Note { content: string; } -export const handler: Handlers = { +export const handler: Handlers = { async GET(request, context) { console.debug({ request, context }); - const result = await query("select * from notes"); + const result = await query("select * from note order by created_at desc"); if (result == null) throw "unable to fetch from database"; const notes = result.rows as Note[]; console.debug(notes); @@ -18,21 +19,27 @@ export const handler: Handlers = { }, }; -export default function Page({ data }: PageProps) { +export default function NotesPage({ data: notes }: PageProps) { return ( - + List of Notes - - Back to Index - Create a note: - - - Post + + + + Post - - {JSON.stringify(data, null, 2)} - - + {notes.map(({ id, content, created_at }) => ( + + + Note {id}{" "} + created at {created_at.toLocaleString()} + + + {content} + + + ))} + ); } diff --git a/routes/note/[id].tsx b/routes/note/[id].tsx new file mode 100644 index 0000000..6cf1189 --- /dev/null +++ b/routes/note/[id].tsx @@ -0,0 +1,37 @@ +import { Handlers, PageProps } from "$fresh/server.ts"; +import { query } from "../../db.ts"; +import { Page } from "../../components/Page.tsx"; + +interface Note { + id: string; + created_at: Date; + content: string; +} + +export const handler: Handlers = { + async GET(request, context) { + console.debug({ request, context }); + const result = await query( + "select * from note where id = $1 order by created_at desc", + [context.params["id"]], + ); + if (result == null) throw "unable to fetch from database"; + const [note] = result.rows as Note[]; + return await context.render(note); + }, +}; + +export default function NotesPage( + { data: { id, created_at, content } }: PageProps, +) { + return ( + + Note {id} created at {created_at.toLocaleString()} + + + {content} + + + + ); +} diff --git a/routes/note/create.tsx b/routes/note/create.tsx new file mode 100644 index 0000000..a8f5ae6 --- /dev/null +++ b/routes/note/create.tsx @@ -0,0 +1,29 @@ +import { Handlers, PageProps } from "$fresh/server.ts"; +import { query } from "../../db.ts"; +import { Page } from "../../components/Page.tsx"; + +type NoteID = string; + +export const handler: Handlers = { + async POST(request, context) { + const content = (await request.formData()).get("content"); + if (!content) throw "no content provided"; + const result = await query( + "insert into note (content) values ($1) returning id", + [content], + ); + if (!result) throw "insert failed"; + const { rows: [{ id }] } = result; + return await context.render(id); + }, +}; + +export default function NotesPage({ data: noteId }: PageProps) { + return ( + + You created a note! + Back to notes + View your note + + ); +} diff --git a/routes/register.tsx b/routes/register.tsx index 5dea34f..1354103 100644 --- a/routes/register.tsx +++ b/routes/register.tsx @@ -1,11 +1,79 @@ -export default function Register() { +import { Handlers, PageProps } from "$fresh/server.ts"; +import { Page } from "../components/Page.tsx"; +import { PostgresError, query } from "../db.ts"; +import { hash } from "https://deno.land/x/bcrypt@v0.4.1/mod.ts"; + +type UserID = string; + +interface RegistrationError { + message: string; +} + +export const handler: Handlers = { + async POST(request, context) { + const formData = (await request.formData()); + const username = formData.get("username"); + const password = formData.get("password"); + if (!username) throw "no username provided"; + if (!password) throw "no password provided"; + const hashed_password = await hash(password.toString()); + try { + const result = await query<{ id: string }>( + `insert into "user" (username, hashed_password) values ($1, $2) returning id`, + [username, hashed_password], + ); + console.debug(result); + if (!result) throw "insert failed"; + const { rows: [{ id }] } = result; + return await context.render(id); + } catch (err) { + if (err instanceof PostgresError) { + console.error("PostgresError:", err); + return await context.render({ message: err.message }); + } else { + throw err; + } + } + }, +}; + +export default function Register( + { data: userId }: PageProps, +) { + if (typeof userId == "string") { + return RegistrationSuccessful(userId); + } else { + return RegistrationForm(userId); + } +} + +function RegistrationSuccessful(_userId: UserID) { return ( - - Username - - Password - - - + + + You're all signed up! Let's go log in! + + + ); +} + +function RegistrationForm(props?: RegistrationError | null) { + console.log(props); + return ( + + {props != null && + {props.message}} + + Username + + Password + + + + ); } diff --git a/twind.config.ts b/twind.config.ts index f7e89b9..360eea8 100644 --- a/twind.config.ts +++ b/twind.config.ts @@ -5,6 +5,6 @@ import { apply } from "twind"; export default { selfURL: import.meta.url, preflight: { - body: apply("bg-white text-black dark:bg-gray-900 dark:text-white"), + body: apply("bg-white text-black dark:(bg-gray-900 text-white)"), }, } as Options;
Create a note:
- {JSON.stringify(data, null, 2)} -
+ {content} +
{content}
+ You're all signed up! Let's go log in! +
{props.message}