diff --git a/README.md b/README.md new file mode 100644 index 0000000..3fa6c04 --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +# Quot diff --git a/app/.dockerignore b/app/.dockerignore new file mode 120000 index 0000000..3e4e48b --- /dev/null +++ b/app/.dockerignore @@ -0,0 +1 @@ +.gitignore \ No newline at end of file diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..6ed48a9 --- /dev/null +++ b/app/.gitignore @@ -0,0 +1,2 @@ +.env +node_modules diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..8710b6e --- /dev/null +++ b/app/Dockerfile @@ -0,0 +1,5 @@ +FROM node:18.7.0-alpine +WORKDIR /app +COPY . /app +RUN npm ci --production +CMD ["npm", "start"] diff --git a/app/compose.yml b/app/compose.yml new file mode 100644 index 0000000..c713e06 --- /dev/null +++ b/app/compose.yml @@ -0,0 +1,10 @@ +services: + app: + image: kou029w/quot-app + build: "." + restart: unless-stopped + init: true + user: ${UID:-1000}:${GID:-1000} + ports: ["8080:8080"] + volumes: + - ".:/app" diff --git a/app/package-lock.json b/app/package-lock.json new file mode 100644 index 0000000..61b6f08 --- /dev/null +++ b/app/package-lock.json @@ -0,0 +1,189 @@ +{ + "name": "@quot/app", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "@quot/app", + "version": "0.0.0", + "dependencies": { + "@exampledev/new.css": "^1.1.3", + "esbuild": "^0.15.5", + "esbuild-register": "^3.3.3", + "solid-js": "^1.4.8" + }, + "devDependencies": { + "@tsconfig/node18-strictest-esm": "^1.0.0", + "@types/node": "^18.7.8", + "typescript": "^4.7.4" + }, + "engines": { + "node": "^18.7.0" + } + }, + "node_modules/@exampledev/new.css": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@exampledev/new.css/-/new.css-1.1.3.tgz", + "integrity": "sha512-qhbGfqBRwUlM6MCSaJdUfjq86opNCMvM+6kVvs6S0kYhy0V8dKbe4rDMIklEJGuMc5QH5OuPjdCReu9I0tim2w==" + }, + "node_modules/@tsconfig/node18-strictest-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tsconfig/node18-strictest-esm/-/node18-strictest-esm-1.0.0.tgz", + "integrity": "sha512-4lY2mZXGFaW13OYcz6kwWFussLbIAg5XBlS2h72jzr4mqr/CuFmF04S7hkpBYbw0k/TNQ4tFLx1/j6VpBqr3Tg==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.8.tgz", + "integrity": "sha512-/YP55EMK2341JkODUb8DM9O0x1SIz2aBvyF33Uf1c76St3VpsMXEIW0nxuKkq/5cxnbz0RD9cfwNZHEAZQD3ag==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" + } + }, + "node_modules/esbuild-linux-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/esbuild-register": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.3.3.tgz", + "integrity": "sha512-eFHOkutgIMJY5gc8LUp/7c+LLlDqzNi9T6AwCZ2WKKl3HmT+5ef3ZRyPPxDOynInML0fgaC50yszPKfPnjC0NQ==", + "peerDependencies": { + "esbuild": ">=0.12 <1" + } + }, + "node_modules/solid-js": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.8.tgz", + "integrity": "sha512-XErZdnnYYXF7OwGSUAPcua2y5/ELB/c53zFCpWiEGqxTNoH1iQghzI8EsHJXk06sNn+Z/TGhb8bPDNNGSgimag==" + }, + "node_modules/typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=4.2.0" + } + } + }, + "dependencies": { + "@exampledev/new.css": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@exampledev/new.css/-/new.css-1.1.3.tgz", + "integrity": "sha512-qhbGfqBRwUlM6MCSaJdUfjq86opNCMvM+6kVvs6S0kYhy0V8dKbe4rDMIklEJGuMc5QH5OuPjdCReu9I0tim2w==" + }, + "@tsconfig/node18-strictest-esm": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tsconfig/node18-strictest-esm/-/node18-strictest-esm-1.0.0.tgz", + "integrity": "sha512-4lY2mZXGFaW13OYcz6kwWFussLbIAg5XBlS2h72jzr4mqr/CuFmF04S7hkpBYbw0k/TNQ4tFLx1/j6VpBqr3Tg==", + "dev": true + }, + "@types/node": { + "version": "18.7.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.7.8.tgz", + "integrity": "sha512-/YP55EMK2341JkODUb8DM9O0x1SIz2aBvyF33Uf1c76St3VpsMXEIW0nxuKkq/5cxnbz0RD9cfwNZHEAZQD3ag==", + "dev": true + }, + "esbuild": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.5.tgz", + "integrity": "sha512-VSf6S1QVqvxfIsSKb3UKr3VhUCis7wgDbtF4Vd9z84UJr05/Sp2fRKmzC+CSPG/dNAPPJZ0BTBLTT1Fhd6N9Gg==", + "requires": { + "@esbuild/linux-loong64": "0.15.5", + "esbuild-android-64": "0.15.5", + "esbuild-android-arm64": "0.15.5", + "esbuild-darwin-64": "0.15.5", + "esbuild-darwin-arm64": "0.15.5", + "esbuild-freebsd-64": "0.15.5", + "esbuild-freebsd-arm64": "0.15.5", + "esbuild-linux-32": "0.15.5", + "esbuild-linux-64": "0.15.5", + "esbuild-linux-arm": "0.15.5", + "esbuild-linux-arm64": "0.15.5", + "esbuild-linux-mips64le": "0.15.5", + "esbuild-linux-ppc64le": "0.15.5", + "esbuild-linux-riscv64": "0.15.5", + "esbuild-linux-s390x": "0.15.5", + "esbuild-netbsd-64": "0.15.5", + "esbuild-openbsd-64": "0.15.5", + "esbuild-sunos-64": "0.15.5", + "esbuild-windows-32": "0.15.5", + "esbuild-windows-64": "0.15.5", + "esbuild-windows-arm64": "0.15.5" + } + }, + "esbuild-linux-64": { + "version": "0.15.5", + "resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.5.tgz", + "integrity": "sha512-ne0GFdNLsm4veXbTnYAWjbx3shpNKZJUd6XpNbKNUZaNllDZfYQt0/zRqOg0sc7O8GQ+PjSMv9IpIEULXVTVmg==", + "optional": true + }, + "esbuild-register": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.3.3.tgz", + "integrity": "sha512-eFHOkutgIMJY5gc8LUp/7c+LLlDqzNi9T6AwCZ2WKKl3HmT+5ef3ZRyPPxDOynInML0fgaC50yszPKfPnjC0NQ==", + "requires": {} + }, + "solid-js": { + "version": "1.4.8", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.4.8.tgz", + "integrity": "sha512-XErZdnnYYXF7OwGSUAPcua2y5/ELB/c53zFCpWiEGqxTNoH1iQghzI8EsHJXk06sNn+Z/TGhb8bPDNNGSgimag==" + }, + "typescript": { + "version": "4.7.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.4.tgz", + "integrity": "sha512-C0WQT0gezHuw6AdY1M2jxUO83Rjf0HP7Sk1DtXj6j1EwkQNZrHAg2XPWlq62oqEhYvONq5pkC2Y9oPljWToLmQ==", + "dev": true + } + } +} diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..523811d --- /dev/null +++ b/app/package.json @@ -0,0 +1,24 @@ +{ + "name": "@quot/app", + "version": "0.0.0", + "private": true, + "scripts": { + "test": "tsc --noEmit", + "start": "node -r esbuild-register src/server.ts" + }, + "engines": { + "node": "^18.7.0" + }, + "packageManager": "npm@8.15.0", + "dependencies": { + "@exampledev/new.css": "^1.1.3", + "esbuild": "^0.15.5", + "esbuild-register": "^3.3.3", + "solid-js": "^1.4.8" + }, + "devDependencies": { + "@tsconfig/node18-strictest-esm": "^1.0.0", + "@types/node": "^18.7.8", + "typescript": "^4.7.4" + } +} diff --git a/app/src/app.tsx b/app/src/app.tsx new file mode 100644 index 0000000..b960e5d --- /dev/null +++ b/app/src/app.tsx @@ -0,0 +1,38 @@ +import { createSignal } from "solid-js"; +import Index from "./pages/index"; +import Page from "./pages/page"; + +const routes = { + "/": Index, +}; + +export default () => { + const [pathname, setPathname] = createSignal( + document.location.pathname as keyof typeof routes + ); + + document.body.addEventListener("click", (e) => { + if ( + e.target instanceof HTMLAnchorElement && + e.target.origin === document.location.origin && + e.target.pathname in routes // TODO: support params ... solid router を入れよう + ) { + e.preventDefault(); + window.history.pushState({}, "", e.target.href); + setPathname(e.target.pathname as keyof typeof routes); + } + }); + + window.addEventListener("popstate", () => { + setPathname(document.location.pathname as keyof typeof routes); + }); + + return () => ( + <> +
+

Quot

+
+ {routes[pathname()] ?? } + + ); +}; diff --git a/app/src/components/editor.tsx b/app/src/components/editor.tsx new file mode 100644 index 0000000..f9c0931 --- /dev/null +++ b/app/src/components/editor.tsx @@ -0,0 +1 @@ +export default () => <>Editor (WIP); diff --git a/app/src/env.d.ts b/app/src/env.d.ts new file mode 100644 index 0000000..5e56e54 --- /dev/null +++ b/app/src/env.d.ts @@ -0,0 +1,5 @@ +interface ImportMeta { + env: { + QUOT_API_URL: string; + }; +} diff --git a/app/src/index.html b/app/src/index.html new file mode 100644 index 0000000..5882806 --- /dev/null +++ b/app/src/index.html @@ -0,0 +1,11 @@ + + + + + + Quot + + + + + diff --git a/app/src/index.ts b/app/src/index.ts new file mode 100644 index 0000000..79dec97 --- /dev/null +++ b/app/src/index.ts @@ -0,0 +1,5 @@ +import "@exampledev/new.css"; +import { render } from "solid-js/web"; +import App from "./app"; + +render(App, document.body); diff --git a/app/src/pages/index.tsx b/app/src/pages/index.tsx new file mode 100644 index 0000000..5165944 --- /dev/null +++ b/app/src/pages/index.tsx @@ -0,0 +1,27 @@ +import { createResource } from "solid-js"; + +type PagesResponse = Array<{ + id: number; + title: string; + text: string; + created: string; + updated: string; +}>; + +async function fetchPages(): Promise { + const res = await fetch( + new URL("/pages?order=updated.desc", import.meta.env.QUOT_API_URL) + ); + const data = await res.json(); + return data as PagesResponse; +} + +export default () => { + const [pages] = createResource("pages", fetchPages); + + return ( +
+
{() => JSON.stringify(pages(), null, " ")}
+
+ ); +}; diff --git a/app/src/pages/page.tsx b/app/src/pages/page.tsx new file mode 100644 index 0000000..5f6c3c0 --- /dev/null +++ b/app/src/pages/page.tsx @@ -0,0 +1,29 @@ +import { createResource } from "solid-js"; +import Editor from "../components/editor"; + +type PageResponse = { + id: number; + title: string; + text: string; + created: string; + updated: string; +}; + +async function fetchPage(id: number): Promise { + const res = await fetch( + new URL(`/pages?id=eq.${id}`, import.meta.env.QUOT_API_URL) + ); + const [data] = await res.json(); + return data as PageResponse; +} + +export default (props: { id: number }) => { + const [page] = createResource(props.id, fetchPage); + + return ( +
+ +
{() => JSON.stringify(page(), null, " ")}
+
+ ); +}; diff --git a/app/src/server.ts b/app/src/server.ts new file mode 100644 index 0000000..6127704 --- /dev/null +++ b/app/src/server.ts @@ -0,0 +1,56 @@ +import type { AddressInfo } from "node:net"; +import http from "node:http"; +import fs from "node:fs"; +import esbuild from "esbuild"; + +async function main() { + const port = Number(process.env.PORT ?? "8080"); + const apiUrl = process.env.QUOT_API_URL || "http://localhost:3000/"; + const publicDir = __dirname; + const htmlPath = `${publicDir}/index.html`; + const scriptPath = `${publicDir}/index.ts`; + + const result = await esbuild.serve( + { + host: "127.0.0.1", + servedir: publicDir, + }, + { + bundle: true, + minify: true, + entryPoints: [scriptPath], + define: { + "import.meta.env.QUOT_API_URL": JSON.stringify(apiUrl), + }, + } + ); + + const handler: http.RequestListener = (req, res) => { + const options = { + hostname: result.host, + port: result.port, + path: req.url, + method: req.method, + headers: req.headers, + }; + + const proxyReq = http.request(options, (proxyRes) => { + if (proxyRes.statusCode === 200) { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + } else { + res.writeHead(200, { "Content-Type": "text/html" }); + fs.createReadStream(htmlPath).pipe(res); + } + }); + + req.pipe(proxyReq); + }; + + const server = http.createServer(handler).listen(port); + const address = server.address() as AddressInfo; + + console.log(`http://0.0.0.0:${address.port}`); +} + +main(); diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..1241306 --- /dev/null +++ b/app/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "@tsconfig/node18-strictest-esm/tsconfig.json", + "compilerOptions": { + "lib": ["dom", "dom.iterable"], + "jsx": "react-jsx", + "jsxImportSource": "solid-js/h", + "noPropertyAccessFromIndexSignature": false + } +} diff --git a/db/.gitignore b/db/.gitignore new file mode 100644 index 0000000..4c49bd7 --- /dev/null +++ b/db/.gitignore @@ -0,0 +1 @@ +.env diff --git a/db/compose.yml b/db/compose.yml new file mode 100644 index 0000000..d163993 --- /dev/null +++ b/db/compose.yml @@ -0,0 +1,37 @@ +services: + api: + image: postgrest/postgrest:v10.0.0 + restart: unless-stopped + ports: ["127.0.0.1:3000:3000"] + environment: + PGRST_DB_URI: postgresql://postgres:${POSTGRES_PASSWORD}@/postgres?host=/var/run/postgresql + PGRST_DB_SCHEMA: public + PGRST_DB_ANON_ROLE: postgres + volumes: + - postgres_socket:/var/run/postgresql + depends_on: + db: + condition: service_healthy + dbmate: + image: amacneil/dbmate:1.15 + command: --wait up + user: ${UID:-1000}:${GID:-1000} + environment: + DATABASE_URL: postgresql://postgres:${POSTGRES_PASSWORD}@/postgres?host=/var/run/postgresql + volumes: + - .:/db + - postgres_socket:/var/run/postgresql + depends_on: [db] + db: + image: postgres:14-alpine + restart: unless-stopped + healthcheck: + test: pg_isready -U postgres + environment: + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + volumes: + - postgres_socket:/var/run/postgresql + - postgres_data_v14:/var/lib/postgresql/data +volumes: + postgres_socket: + postgres_data_v14: diff --git a/db/migrations/0_init.sql b/db/migrations/0_init.sql new file mode 100644 index 0000000..31fd357 --- /dev/null +++ b/db/migrations/0_init.sql @@ -0,0 +1,23 @@ +-- migrate:up +CREATE FUNCTION update_timestamp() RETURNS trigger LANGUAGE plpgsql AS $$ + BEGIN + NEW.updated = CURRENT_TIMESTAMP; + RETURN NEW; + END +$$; + +CREATE TABLE pages ( + id SERIAL PRIMARY KEY, + title TEXT NOT NULL UNIQUE, + text TEXT NOT NULL, + created TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP, + updated TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP +); + +CREATE TRIGGER pages_updated BEFORE UPDATE ON pages FOR EACH ROW + EXECUTE PROCEDURE update_timestamp(); + +-- migrate:down +DROP TRIGGER pages_updated ON pages; +DROP TABLE pages; +DROP FUNCTION update_timestamp(); diff --git a/db/schema.sql b/db/schema.sql new file mode 100644 index 0000000..af08767 --- /dev/null +++ b/db/schema.sql @@ -0,0 +1,120 @@ +SET statement_timeout = 0; +SET lock_timeout = 0; +SET idle_in_transaction_session_timeout = 0; +SET client_encoding = 'UTF8'; +SET standard_conforming_strings = on; +SELECT pg_catalog.set_config('search_path', '', false); +SET check_function_bodies = false; +SET xmloption = content; +SET client_min_messages = warning; +SET row_security = off; + +-- +-- Name: update_timestamp(); Type: FUNCTION; Schema: public; Owner: - +-- + +CREATE FUNCTION public.update_timestamp() RETURNS trigger + LANGUAGE plpgsql + AS $$ + BEGIN + NEW.updated = CURRENT_TIMESTAMP; + RETURN NEW; + END +$$; + + +SET default_tablespace = ''; + +SET default_table_access_method = heap; + +-- +-- Name: pages; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.pages ( + id integer NOT NULL, + title text NOT NULL, + text text NOT NULL, + created timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL, + updated timestamp with time zone DEFAULT CURRENT_TIMESTAMP NOT NULL +); + + +-- +-- Name: pages_id_seq; Type: SEQUENCE; Schema: public; Owner: - +-- + +CREATE SEQUENCE public.pages_id_seq + AS integer + START WITH 1 + INCREMENT BY 1 + NO MINVALUE + NO MAXVALUE + CACHE 1; + + +-- +-- Name: pages_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - +-- + +ALTER SEQUENCE public.pages_id_seq OWNED BY public.pages.id; + + +-- +-- Name: schema_migrations; Type: TABLE; Schema: public; Owner: - +-- + +CREATE TABLE public.schema_migrations ( + version character varying(255) NOT NULL +); + + +-- +-- Name: pages id; Type: DEFAULT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pages ALTER COLUMN id SET DEFAULT nextval('public.pages_id_seq'::regclass); + + +-- +-- Name: pages pages_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pages + ADD CONSTRAINT pages_pkey PRIMARY KEY (id); + + +-- +-- Name: pages pages_title_key; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.pages + ADD CONSTRAINT pages_title_key UNIQUE (title); + + +-- +-- Name: schema_migrations schema_migrations_pkey; Type: CONSTRAINT; Schema: public; Owner: - +-- + +ALTER TABLE ONLY public.schema_migrations + ADD CONSTRAINT schema_migrations_pkey PRIMARY KEY (version); + + +-- +-- Name: pages pages_updated; Type: TRIGGER; Schema: public; Owner: - +-- + +CREATE TRIGGER pages_updated BEFORE UPDATE ON public.pages FOR EACH ROW EXECUTE FUNCTION public.update_timestamp(); + + +-- +-- PostgreSQL database dump complete +-- + + +-- +-- Dbmate schema migrations +-- + +INSERT INTO public.schema_migrations (version) VALUES + ('0');