Styles!
This commit is contained in:
parent
aed2cdfa0c
commit
616228c997
|
@ -6,7 +6,6 @@ export function Button(props: JSX.HTMLAttributes<HTMLButtonElement>) {
|
||||||
<button
|
<button
|
||||||
{...props}
|
{...props}
|
||||||
disabled={!IS_BROWSER || props.disabled}
|
disabled={!IS_BROWSER || props.disabled}
|
||||||
class="px-2 py-1 border(gray-100 2) hover:bg-gray-200"
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,15 @@
|
||||||
import { JSX } from "preact";
|
import { JSX } from "preact";
|
||||||
|
|
||||||
const NAV_ITEM_CLASSES =
|
const NAV_ITEM_CLASSES = "flex justify-center items-center px-4 py-2";
|
||||||
"flex justify-center items-center px-4 py-2 text-blue-500 hover:bg-purple-700";
|
|
||||||
|
const HEADER_CLASSES = "bg-gray-200 dark:bg-gray-800";
|
||||||
|
|
||||||
export function Page(props: JSX.HTMLAttributes<HTMLDivElement>) {
|
export function Page(props: JSX.HTMLAttributes<HTMLDivElement>) {
|
||||||
return (
|
return (
|
||||||
<div class="relative min-h-screen flex flex-col">
|
<div class="relative min-h-screen flex flex-col">
|
||||||
<header class="flex justify-start items-center">
|
<header class="flex justify-start items-center">
|
||||||
<nav class="flex bg-gray-800 w-full drop-shadow-md">
|
<nav class={`flex w-full drop-shadow-md ${HEADER_CLASSES}`}>
|
||||||
<a href="/" class={NAV_ITEM_CLASSES}>
|
<a href="/" class={`${NAV_ITEM_CLASSES} text-black dark:text-white`}>
|
||||||
<h1 class="text-2xl">LyricScreen</h1>
|
<h1 class="text-2xl">LyricScreen</h1>
|
||||||
</a>
|
</a>
|
||||||
<a href="/note" class={NAV_ITEM_CLASSES}>Notes</a>
|
<a href="/note" class={NAV_ITEM_CLASSES}>Notes</a>
|
||||||
|
@ -19,7 +20,7 @@ export function Page(props: JSX.HTMLAttributes<HTMLDivElement>) {
|
||||||
<main class="p-2">
|
<main class="p-2">
|
||||||
{props.children}
|
{props.children}
|
||||||
</main>
|
</main>
|
||||||
<footer class="p-2 bg-gray-800 w-full mt-auto">
|
<footer class={`p-2 w-full mt-auto ${HEADER_CLASSES}`}>
|
||||||
"It's a bit much, really..."
|
"It's a bit much, really..."
|
||||||
</footer>
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,13 +1,19 @@
|
||||||
import { query } from "./db.ts";
|
import { query } from "@/db.ts";
|
||||||
|
|
||||||
const id = "id uuid primary key default uuid_generate_v4()";
|
const id = "id uuid primary key default uuid_generate_v4()";
|
||||||
|
|
||||||
|
interface TableSpec {
|
||||||
|
columns: string[];
|
||||||
|
additionalStatements?: string[];
|
||||||
|
prepStatements?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
const timestamps = [
|
const timestamps = [
|
||||||
"created_at timestamptz not null default now()",
|
"created_at timestamptz not null default now()",
|
||||||
"updated_at timestamptz not null default now()",
|
"updated_at timestamptz not null default now()",
|
||||||
];
|
];
|
||||||
|
|
||||||
const tables = {
|
const tables: Record<string, TableSpec> = {
|
||||||
"note": {
|
"note": {
|
||||||
columns: [id, "content text not null", ...timestamps],
|
columns: [id, "content text not null", ...timestamps],
|
||||||
},
|
},
|
||||||
|
@ -16,24 +22,53 @@ const tables = {
|
||||||
id,
|
id,
|
||||||
"username text not null unique",
|
"username text not null unique",
|
||||||
"hashed_password text not null",
|
"hashed_password text not null",
|
||||||
|
"name text",
|
||||||
...timestamps,
|
...timestamps,
|
||||||
],
|
],
|
||||||
constraints: [],
|
},
|
||||||
|
"team": {
|
||||||
|
columns: [
|
||||||
|
id,
|
||||||
|
"name text not null",
|
||||||
|
...timestamps,
|
||||||
|
],
|
||||||
|
additionalStatements: [
|
||||||
|
'create index name_idx on team ("name")',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"team_user": {
|
||||||
|
prepStatements: [
|
||||||
|
"drop type if exists team_user_status",
|
||||||
|
"create type team_user_status as enum ('invited', 'accepted', 'owner')",
|
||||||
|
],
|
||||||
|
columns: [
|
||||||
|
"team_id uuid",
|
||||||
|
"user_id uuid",
|
||||||
|
"status team_user_status",
|
||||||
|
...timestamps,
|
||||||
|
],
|
||||||
|
additionalStatements: [
|
||||||
|
"create index team_user_idx on team_user (team_id) include (user_id)",
|
||||||
|
"create index team_idx on team_user (team_id)",
|
||||||
|
"create index user_idx on team_user (user_id)",
|
||||||
|
"create index status_idx on team_user (status)",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const tableStatements = Object.entries(tables).map(([name, meta]) => `
|
const tableStatements = Object.entries(tables).map(([name, meta]) => `
|
||||||
drop table if exists "${name}";
|
${(meta.prepStatements || []).map((s) => `${s};`).join("\n")}
|
||||||
create table "${name}" (
|
-- TABLE ${name}
|
||||||
|
drop table if exists "${name}";
|
||||||
|
create table "${name}" (
|
||||||
${meta.columns.join(",\n ")}
|
${meta.columns.join(",\n ")}
|
||||||
);
|
);
|
||||||
|
${(meta.additionalStatements || []).map((s) => `${s};`).join("\n")}
|
||||||
`);
|
`);
|
||||||
|
|
||||||
console.log(tableStatements);
|
|
||||||
|
|
||||||
const queryString = `
|
const queryString = `
|
||||||
create extension if not exists "uuid-ossp";
|
create extension if not exists "uuid-ossp";
|
||||||
${tableStatements.map((s) => s.trim()).join("\n\n ")}
|
${tableStatements.map((s) => s.trim()).join("\n\n")}
|
||||||
`;
|
`;
|
||||||
console.log(queryString);
|
console.log(queryString);
|
||||||
await query(queryString);
|
await query(queryString);
|
||||||
|
|
40
fresh.gen.ts
40
fresh.gen.ts
|
@ -2,26 +2,26 @@
|
||||||
// This file SHOULD be checked into source version control.
|
// This file SHOULD be checked into source version control.
|
||||||
// This file is automatically updated during development when running `dev.ts`.
|
// This file is automatically updated during development when running `dev.ts`.
|
||||||
|
|
||||||
import config from "./deno.json" assert { type: "json" };
|
import config from "@/deno.json" assert { type: "json" };
|
||||||
import * as $0 from "./routes/[name].tsx";
|
import * as $0 from "@/routes/[name].tsx";
|
||||||
import * as $1 from "./routes/_404.tsx";
|
import * as $1 from "@/routes/_404.tsx";
|
||||||
import * as $2 from "./routes/_500.tsx";
|
import * as $2 from "@/routes/_500.tsx";
|
||||||
import * as $3 from "./routes/_middleware.ts";
|
import * as $3 from "@/routes/_middleware.ts";
|
||||||
import * as $4 from "./routes/about.tsx";
|
import * as $4 from "@/routes/about.tsx";
|
||||||
import * as $5 from "./routes/api/joke.ts";
|
import * as $5 from "@/routes/api/joke.ts";
|
||||||
import * as $6 from "./routes/api/random-uuid.ts";
|
import * as $6 from "@/routes/api/random-uuid.ts";
|
||||||
import * as $7 from "./routes/countdown.tsx";
|
import * as $7 from "@/routes/countdown.tsx";
|
||||||
import * as $8 from "./routes/github/[username].tsx";
|
import * as $8 from "@/routes/github/[username].tsx";
|
||||||
import * as $9 from "./routes/index.tsx";
|
import * as $9 from "@/routes/index.tsx";
|
||||||
import * as $10 from "./routes/login.tsx";
|
import * as $10 from "@/routes/login.tsx";
|
||||||
import * as $11 from "./routes/note.tsx";
|
import * as $11 from "@/routes/note.tsx";
|
||||||
import * as $12 from "./routes/note/[id].tsx";
|
import * as $12 from "@/routes/note/[id].tsx";
|
||||||
import * as $13 from "./routes/note/create.tsx";
|
import * as $13 from "@/routes/note/create.tsx";
|
||||||
import * as $14 from "./routes/register.tsx";
|
import * as $14 from "@/routes/register.tsx";
|
||||||
import * as $15 from "./routes/route-config-example.tsx";
|
import * as $15 from "@/routes/route-config-example.tsx";
|
||||||
import * as $16 from "./routes/search.tsx";
|
import * as $16 from "@/routes/search.tsx";
|
||||||
import * as $$0 from "./islands/Countdown.tsx";
|
import * as $$0 from "@/islands/Countdown.tsx";
|
||||||
import * as $$1 from "./islands/Counter.tsx";
|
import * as $$1 from "@/islands/Counter.tsx";
|
||||||
|
|
||||||
const manifest = {
|
const manifest = {
|
||||||
routes: {
|
routes: {
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
{
|
{
|
||||||
"imports": {
|
"imports": {
|
||||||
|
"@/": "./",
|
||||||
|
"$std/": "https://deno.land/std@0.144.0/",
|
||||||
"$fresh/": "https://deno.land/x/fresh@1.1.1/",
|
"$fresh/": "https://deno.land/x/fresh@1.1.1/",
|
||||||
"preact": "https://esm.sh/preact@10.11.0",
|
"preact": "https://esm.sh/preact@10.11.0",
|
||||||
"preact/": "https://esm.sh/preact@10.11.0/",
|
"preact/": "https://esm.sh/preact@10.11.0/",
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import { useState } from "preact/hooks";
|
import { useState } from "preact/hooks";
|
||||||
import { Button } from "../components/Button.tsx";
|
import { Button } from "@/components/Button.tsx";
|
||||||
|
|
||||||
interface CounterProps {
|
interface CounterProps {
|
||||||
start: number;
|
start: number;
|
||||||
|
|
4
main.ts
4
main.ts
|
@ -5,9 +5,9 @@
|
||||||
/// <reference lib="deno.ns" />
|
/// <reference lib="deno.ns" />
|
||||||
|
|
||||||
import { start } from "$fresh/server.ts";
|
import { start } from "$fresh/server.ts";
|
||||||
import manifest from "./fresh.gen.ts";
|
import manifest from "@/fresh.gen.ts";
|
||||||
|
|
||||||
import twindPlugin from "$fresh/plugins/twind.ts";
|
import twindPlugin from "$fresh/plugins/twind.ts";
|
||||||
import twindConfig from "./twind.config.ts";
|
import twindConfig from "@/twind.config.ts";
|
||||||
|
|
||||||
await start(manifest, { plugins: [twindPlugin(twindConfig)] });
|
await start(manifest, { plugins: [twindPlugin(twindConfig)] });
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import Counter from "../islands/Counter.tsx";
|
import Counter from "@/islands/Counter.tsx";
|
||||||
import { Page } from "../components/Page.tsx";
|
import { Page } from "@/components/Page.tsx";
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -25,9 +25,9 @@ export default function NotesPage({ data: notes }: PageProps<Note[]>) {
|
||||||
<h1>List of Notes</h1>
|
<h1>List of Notes</h1>
|
||||||
<p>Create a note:</p>
|
<p>Create a note:</p>
|
||||||
<form class="flex flex-col" action="./note/create" method="POST">
|
<form class="flex flex-col" action="./note/create" method="POST">
|
||||||
<textarea rows="6" class="px-4 py-2 bg-gray-800" name="content">
|
<textarea rows={6} class="" name="content">
|
||||||
</textarea>
|
</textarea>
|
||||||
<button class="mt-2 px-4 py-2 bg-gray-800">Post</button>
|
<input class="mt-2" type="submit" value="Post" />
|
||||||
</form>
|
</form>
|
||||||
{notes.map(({ id, content, created_at }) => (
|
{notes.map(({ id, content, created_at }) => (
|
||||||
<div class="my-4" key={id}>
|
<div class="my-4" key={id}>
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Handlers, PageProps } from "$fresh/server.ts";
|
import { Handlers, PageProps } from "$fresh/server.ts";
|
||||||
import { query } from "../../db.ts";
|
import { query } from "@/db.ts";
|
||||||
import { Page } from "../../components/Page.tsx";
|
import { Page } from "@/components/Page.tsx";
|
||||||
|
|
||||||
type NoteID = string;
|
type NoteID = string;
|
||||||
|
|
||||||
|
@ -8,7 +8,7 @@ export const handler: Handlers<NoteID> = {
|
||||||
async POST(request, context) {
|
async POST(request, context) {
|
||||||
const content = (await request.formData()).get("content");
|
const content = (await request.formData()).get("content");
|
||||||
if (!content) throw "no content provided";
|
if (!content) throw "no content provided";
|
||||||
const result = await query(
|
const result = await query<{ id: string }>(
|
||||||
"insert into note (content) values ($1) returning id",
|
"insert into note (content) values ($1) returning id",
|
||||||
[content],
|
[content],
|
||||||
);
|
);
|
||||||
|
|
|
@ -14,8 +14,12 @@ export const handler: Handlers<UserID | RegistrationError | null> = {
|
||||||
const formData = (await request.formData());
|
const formData = (await request.formData());
|
||||||
const username = formData.get("username");
|
const username = formData.get("username");
|
||||||
const password = formData.get("password");
|
const password = formData.get("password");
|
||||||
if (!username) throw "no username provided";
|
if (!username) {
|
||||||
if (!password) throw "no password provided";
|
return await context.render({ message: "no username provided" });
|
||||||
|
}
|
||||||
|
if (!password) {
|
||||||
|
return await context.render({ message: "no password provided" });
|
||||||
|
}
|
||||||
const hashed_password = await hash(password.toString());
|
const hashed_password = await hash(password.toString());
|
||||||
try {
|
try {
|
||||||
const result = await query<{ id: string }>(
|
const result = await query<{ id: string }>(
|
||||||
|
@ -27,12 +31,15 @@ export const handler: Handlers<UserID | RegistrationError | null> = {
|
||||||
const { rows: [{ id }] } = result;
|
const { rows: [{ id }] } = result;
|
||||||
return await context.render(id);
|
return await context.render(id);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof PostgresError) {
|
if (
|
||||||
console.error("PostgresError:", err);
|
err instanceof PostgresError && err.fields.code == "23505" &&
|
||||||
return await context.render({ message: err.message });
|
err.fields.constraint == "user_username_key"
|
||||||
} else {
|
) {
|
||||||
throw err;
|
return await context.render({
|
||||||
|
message: `A user with username '${username}' already exists`,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
throw err;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -62,14 +69,18 @@ function RegistrationForm(props?: RegistrationError | null) {
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
{props != null &&
|
{props != null &&
|
||||||
<p class="text-red-500">{props.message}</p>}
|
(
|
||||||
|
<p class="text-red-500">
|
||||||
|
<strong>Error</strong>: {props.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<form class="flex flex-col max-w-lg" method="post">
|
<form class="flex flex-col max-w-lg" method="post">
|
||||||
<label for="username">Username</label>
|
<label for="username">Username</label>
|
||||||
<input class="bg-gray-800 px-4 py-2" type="text" name="username" />
|
<input type="text" name="username" />
|
||||||
<label for="password">Password</label>
|
<label for="password">Password</label>
|
||||||
<input class="bg-gray-800 px-4 py-2" type="password" name="password" />
|
<input type="password" name="password" />
|
||||||
<input
|
<input
|
||||||
class="bg-gray-800 px-4 p-2 mt-2"
|
class="bg-blue-800 px-4 p-2 mt-2"
|
||||||
type="submit"
|
type="submit"
|
||||||
value="Register"
|
value="Register"
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -1,10 +1,24 @@
|
||||||
import { Options } from "$fresh/plugins/twind.ts";
|
import { Options } from "$fresh/plugins/twind.ts";
|
||||||
|
|
||||||
import { apply } from "twind";
|
import { apply } from "twind";
|
||||||
|
import { css } from "twind/css";
|
||||||
|
|
||||||
|
const button = apply(
|
||||||
|
"bg-blue(400 500(hover:& focus:&) dark:(600 700(hover:& focus:&))) px-4 py-2 rounded cursor-pointer ring(focus:& current)",
|
||||||
|
);
|
||||||
|
|
||||||
|
const input = apply(
|
||||||
|
"rounded bg-gray(200 300(hover:& focus:&) dark:(800 700(hover:& focus:&))) px-4 py-2 ring(focus:& current)",
|
||||||
|
);
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
selfURL: import.meta.url,
|
selfURL: import.meta.url,
|
||||||
preflight: {
|
preflight: (preflight) =>
|
||||||
|
css(preflight, {
|
||||||
body: apply("bg-white text-black dark:(bg-gray-900 text-white)"),
|
body: apply("bg-white text-black dark:(bg-gray-900 text-white)"),
|
||||||
},
|
"body input, body textarea": input,
|
||||||
|
"body button, body input[type=submit]": button,
|
||||||
|
"body a": apply(
|
||||||
|
"text-blue(600 700(hover:&) dark:(400 300(hover:&))",
|
||||||
|
),
|
||||||
|
}),
|
||||||
} as Options;
|
} as Options;
|
||||||
|
|
Loading…
Reference in a new issue