1
0
Fork 0
mirror of https://github.com/kou029w/quot.git synced 2025-01-18 16:08:03 +00:00

create quot project

This commit is contained in:
Nebel 2022-08-23 09:01:47 +09:00
parent 8d76de4274
commit ac30da6b61
20 changed files with 594 additions and 0 deletions

1
README.md Normal file
View file

@ -0,0 +1 @@
# Quot

1
app/.dockerignore Symbolic link
View file

@ -0,0 +1 @@
.gitignore

2
app/.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.env
node_modules

5
app/Dockerfile Normal file
View file

@ -0,0 +1,5 @@
FROM node:18.7.0-alpine
WORKDIR /app
COPY . /app
RUN npm ci --production
CMD ["npm", "start"]

10
app/compose.yml Normal file
View file

@ -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"

189
app/package-lock.json generated Normal file
View file

@ -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
}
}
}

24
app/package.json Normal file
View file

@ -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"
}
}

38
app/src/app.tsx Normal file
View file

@ -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 () => (
<>
<header>
<h1>Quot</h1>
</header>
{routes[pathname()] ?? <Page id={Number(pathname().slice(1))} />}
</>
);
};

View file

@ -0,0 +1 @@
export default () => <>Editor (WIP)</>;

5
app/src/env.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
interface ImportMeta {
env: {
QUOT_API_URL: string;
};
}

11
app/src/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="ja" dir="ltr">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<title>Quot</title>
<link rel="stylesheet" href="/index.css" />
<script type="module" src="/index.js"></script>
</head>
<body></body>
</html>

5
app/src/index.ts Normal file
View file

@ -0,0 +1,5 @@
import "@exampledev/new.css";
import { render } from "solid-js/web";
import App from "./app";
render(App, document.body);

27
app/src/pages/index.tsx Normal file
View file

@ -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<PagesResponse> {
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 (
<main>
<pre>{() => JSON.stringify(pages(), null, " ")}</pre>
</main>
);
};

29
app/src/pages/page.tsx Normal file
View file

@ -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<PageResponse> {
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 (
<main>
<Editor />
<pre>{() => JSON.stringify(page(), null, " ")}</pre>
</main>
);
};

56
app/src/server.ts Normal file
View file

@ -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();

9
app/tsconfig.json Normal file
View file

@ -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
}
}

1
db/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
.env

37
db/compose.yml Normal file
View file

@ -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:

23
db/migrations/0_init.sql Normal file
View file

@ -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();

120
db/schema.sql Normal file
View file

@ -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');