create-evikit 2.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CNAME +1 -0
- package/LICENSE +12 -0
- package/bin/create-evikit.js +193 -0
- package/eslint.config.js +22 -0
- package/index.css +55 -0
- package/index.html +24 -0
- package/index.js +19 -0
- package/package.json +52 -0
- package/po/create-evikit.pot +84 -0
- package/po/uk.po +100 -0
- package/src/api.js +16 -0
- package/src/app.js +24 -0
- package/src/lib/db/index.js +19 -0
- package/src/lib/db/schema.js +8 -0
- package/src/lib/index.js +5 -0
- package/src/routes/api/server.js +29 -0
- package/src/routes/api/todo/server.js +21 -0
- package/src/routes/api/todo/types.js +13 -0
- package/src/routes/api/types.js +6 -0
- package/src/routes/page.js +260 -0
- package/src/routes/todo/page.js +55 -0
- package/tsconfig.json +30 -0
- package/vite.config.js +20 -0
package/CNAME
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
evikit.js.org
|
package/LICENSE
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
Copyright (C) 2026 by Denys Nykula <nykula@ukr.net>
|
|
2
|
+
|
|
3
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
4
|
+
purpose with or without fee is hereby granted.
|
|
5
|
+
|
|
6
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
|
7
|
+
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
|
8
|
+
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
|
9
|
+
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
|
10
|
+
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
|
11
|
+
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
|
12
|
+
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { execSync } from "node:child_process";
|
|
3
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
import ownPackage from "../package.json" with { type: "json" };
|
|
8
|
+
import { template } from "../src/routes/api/server.js";
|
|
9
|
+
|
|
10
|
+
const ownRoot = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
11
|
+
const name = process.argv[2];
|
|
12
|
+
if (!name || !name[0] || /^[/-]/.test(name[0])) {
|
|
13
|
+
console.log("Try: npx create-evikit my-app");
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const target = resolve(process.cwd(), name);
|
|
18
|
+
if (existsSync(target)) {
|
|
19
|
+
mkdirSync(target, { recursive: true });
|
|
20
|
+
}
|
|
21
|
+
for (const file of [
|
|
22
|
+
...template,
|
|
23
|
+
".gitignore",
|
|
24
|
+
"eslint.config.js",
|
|
25
|
+
"tsconfig.json",
|
|
26
|
+
]) {
|
|
27
|
+
const path = join(target, file);
|
|
28
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
29
|
+
writeFileSync(
|
|
30
|
+
path,
|
|
31
|
+
readFileSync(join(ownRoot, file), "utf8").replace(
|
|
32
|
+
/<link\s+href="\/node_modules\/[^"]+.css"\s+media="[^"]+"\s+rel="stylesheet"\s+\/>\s+/g,
|
|
33
|
+
"",
|
|
34
|
+
),
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// ---
|
|
39
|
+
|
|
40
|
+
writeFileSync(
|
|
41
|
+
join(target, "src/lib/index.js"),
|
|
42
|
+
`
|
|
43
|
+
import acceptLanguage from "accept-language";
|
|
44
|
+
|
|
45
|
+
export function acceptLanguages() {
|
|
46
|
+
acceptLanguage.languages(["en"]);
|
|
47
|
+
}
|
|
48
|
+
`.trimStart(),
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// ---
|
|
52
|
+
|
|
53
|
+
writeFileSync(
|
|
54
|
+
join(target, "src/routes/api/types.js"),
|
|
55
|
+
`
|
|
56
|
+
import { object, string } from "valibot";
|
|
57
|
+
|
|
58
|
+
export const HomeGet = {
|
|
59
|
+
input: object({ lang: string() }),
|
|
60
|
+
outputs: { 200: object({ name: string(), version: string() }) },
|
|
61
|
+
};
|
|
62
|
+
`.trimStart(),
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
// ---
|
|
66
|
+
|
|
67
|
+
writeFileSync(
|
|
68
|
+
join(target, "src/routes/api/server.js"),
|
|
69
|
+
`
|
|
70
|
+
import { endpoint, json } from "evikit/server";
|
|
71
|
+
|
|
72
|
+
import pkg from "../../../package.json" with { type: "json" };
|
|
73
|
+
import { HomeGet } from "./types.js";
|
|
74
|
+
|
|
75
|
+
export const HomeServer = {
|
|
76
|
+
get: endpoint(HomeGet, async () =>
|
|
77
|
+
json({ name: pkg.name, version: pkg.version }),
|
|
78
|
+
),
|
|
79
|
+
};
|
|
80
|
+
`.trimStart(),
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
// ---
|
|
84
|
+
|
|
85
|
+
writeFileSync(
|
|
86
|
+
join(target, "src/routes/page.js"),
|
|
87
|
+
`
|
|
88
|
+
import { a, h1, main, p, useApi, useI18n } from "evikit";
|
|
89
|
+
import { useTitle } from "hoofd/preact";
|
|
90
|
+
|
|
91
|
+
import { HomeGet } from "./api/types.js";
|
|
92
|
+
|
|
93
|
+
export function HomePage() {
|
|
94
|
+
const i18n = useI18n();
|
|
95
|
+
const { data } = useApi(HomeGet);
|
|
96
|
+
const title = data?.name || i18n.gettext("Loading...");
|
|
97
|
+
useTitle(title);
|
|
98
|
+
|
|
99
|
+
return main(
|
|
100
|
+
{ class: "p-8" },
|
|
101
|
+
h1({ class: "text-xl" }, title),
|
|
102
|
+
data ? p({ class: "my-2" }, "v" + data.version) : undefined,
|
|
103
|
+
a({ class: "link", href: "/todo" }, i18n.gettext("Todos")),
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
`.trimStart(),
|
|
107
|
+
);
|
|
108
|
+
|
|
109
|
+
// ---
|
|
110
|
+
|
|
111
|
+
writeFileSync(
|
|
112
|
+
join(target, "index.css"),
|
|
113
|
+
`
|
|
114
|
+
@import "tailwindcss";
|
|
115
|
+
@plugin "daisyui";
|
|
116
|
+
`.trimStart(),
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
// ---
|
|
120
|
+
|
|
121
|
+
writeFileSync(
|
|
122
|
+
join(target, ".env"),
|
|
123
|
+
`
|
|
124
|
+
ORIGIN=http://localhost:8000
|
|
125
|
+
PORT=8000
|
|
126
|
+
`.trimStart(),
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
// ---
|
|
130
|
+
|
|
131
|
+
const { allowScripts, dependencies, devDependencies, scripts } = ownPackage;
|
|
132
|
+
/* eslint-disable perfectionist/sort-objects */
|
|
133
|
+
const pkg = {
|
|
134
|
+
name,
|
|
135
|
+
version: "0.1.0",
|
|
136
|
+
private: true,
|
|
137
|
+
scripts: {
|
|
138
|
+
dev: scripts.dev,
|
|
139
|
+
build: scripts.build,
|
|
140
|
+
start: scripts.start,
|
|
141
|
+
extract: scripts.extract,
|
|
142
|
+
format: scripts.format,
|
|
143
|
+
lint: scripts.lint,
|
|
144
|
+
test: "node --test --test-concurrency=1",
|
|
145
|
+
},
|
|
146
|
+
type: "module",
|
|
147
|
+
dependencies: {
|
|
148
|
+
"accept-language": dependencies["accept-language"],
|
|
149
|
+
classnames: dependencies.classnames,
|
|
150
|
+
evikit: dependencies.evikit,
|
|
151
|
+
express: dependencies.express,
|
|
152
|
+
"file-type": dependencies["file-type"],
|
|
153
|
+
hoofd: dependencies.hoofd,
|
|
154
|
+
preact: dependencies.preact,
|
|
155
|
+
"preact-iso": dependencies["preact-iso"],
|
|
156
|
+
"swagger-ui-express": dependencies["swagger-ui-express"],
|
|
157
|
+
valibot: dependencies.valibot,
|
|
158
|
+
},
|
|
159
|
+
devDependencies: {
|
|
160
|
+
"@eslint/compat": devDependencies["@eslint/compat"],
|
|
161
|
+
"@eslint/js": devDependencies["@eslint/js"],
|
|
162
|
+
"@preact/preset-vite": devDependencies["@preact/preset-vite"],
|
|
163
|
+
"@tailwindcss/vite": devDependencies["@tailwindcss/vite"],
|
|
164
|
+
"@types/express": devDependencies["@types/express"],
|
|
165
|
+
"@types/node": devDependencies["@types/node"],
|
|
166
|
+
"@types/swagger-ui-express": devDependencies["@types/swagger-ui-express"],
|
|
167
|
+
"cross-env": devDependencies["cross-env"],
|
|
168
|
+
daisyui: devDependencies.daisyui,
|
|
169
|
+
eslint: devDependencies.eslint,
|
|
170
|
+
"eslint-config-prettier": devDependencies["eslint-config-prettier"],
|
|
171
|
+
"eslint-plugin-perfectionist":
|
|
172
|
+
devDependencies["eslint-plugin-perfectionist"],
|
|
173
|
+
globals: devDependencies.globals,
|
|
174
|
+
prettier: devDependencies.prettier,
|
|
175
|
+
typescript: devDependencies.typescript,
|
|
176
|
+
"typescript-eslint": devDependencies["typescript-eslint"],
|
|
177
|
+
vite: devDependencies.vite,
|
|
178
|
+
"vite-bundle-analyzer": devDependencies["vite-bundle-analyzer"],
|
|
179
|
+
},
|
|
180
|
+
allowScripts,
|
|
181
|
+
};
|
|
182
|
+
/* eslint-enable perfectionist/sort-objects */
|
|
183
|
+
writeFileSync(
|
|
184
|
+
join(target, "package.json"),
|
|
185
|
+
JSON.stringify(pkg, null, 2) + "\n",
|
|
186
|
+
);
|
|
187
|
+
execSync("npm install", { cwd: target, stdio: "inherit" });
|
|
188
|
+
|
|
189
|
+
console.log("\n✅ Project created successfully!\n");
|
|
190
|
+
console.log("Next steps:");
|
|
191
|
+
console.log(` cd ${name}`);
|
|
192
|
+
console.log(` npm run dev\n`);
|
|
193
|
+
console.log("Documentation: https://codeberg.org/nykula/evikit");
|
package/eslint.config.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { includeIgnoreFile } from "@eslint/compat";
|
|
2
|
+
import js from "@eslint/js";
|
|
3
|
+
import prettier from "eslint-config-prettier/flat";
|
|
4
|
+
import perfectionist from "eslint-plugin-perfectionist";
|
|
5
|
+
import { defineConfig } from "eslint/config";
|
|
6
|
+
import globals from "globals";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
import ts from "typescript-eslint";
|
|
9
|
+
|
|
10
|
+
export default defineConfig(
|
|
11
|
+
includeIgnoreFile(fileURLToPath(new URL("./.gitignore", import.meta.url))),
|
|
12
|
+
js.configs.recommended,
|
|
13
|
+
...ts.configs.strict,
|
|
14
|
+
...ts.configs.stylistic,
|
|
15
|
+
perfectionist.configs["recommended-natural"],
|
|
16
|
+
prettier,
|
|
17
|
+
{
|
|
18
|
+
languageOptions: {
|
|
19
|
+
globals: { ...globals.browser, ...globals.node },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
);
|
package/index.css
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
@plugin "daisyui";
|
|
3
|
+
|
|
4
|
+
[lang="uk"] p {
|
|
5
|
+
text-wrap: balance;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
p a:not([class]) {
|
|
9
|
+
color: #28f;
|
|
10
|
+
text-decoration: underline;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
figcaption span,
|
|
14
|
+
summary span {
|
|
15
|
+
opacity: 0.3333;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
[class*="shj-lang-"] {
|
|
19
|
+
box-shadow: none !important;
|
|
20
|
+
font: inherit !important;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.shj-inline {
|
|
24
|
+
padding: 0 !important;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
figure,
|
|
28
|
+
details {
|
|
29
|
+
box-shadow: 0 0 1em rgba(0, 0, 0, 0.05);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
nav a {
|
|
33
|
+
background-color: currentColor;
|
|
34
|
+
color: #fff;
|
|
35
|
+
border-radius: 1em;
|
|
36
|
+
box-shadow: 0 0 5px rgba(0, 0, 0, 0.05);
|
|
37
|
+
display: inline-block;
|
|
38
|
+
margin-bottom: 1em;
|
|
39
|
+
padding: 1em;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
nav a:focus,
|
|
43
|
+
nav a:hover {
|
|
44
|
+
opacity: 0.75;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
nav a[href*="documentation"] {
|
|
48
|
+
background-color: #84f;
|
|
49
|
+
color: #fff;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
nav a[href*="issues"] {
|
|
53
|
+
background-color: #7d8f;
|
|
54
|
+
color: #fff;
|
|
55
|
+
}
|
package/index.html
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<meta name="color-scheme" content="dark light" />
|
|
7
|
+
<link href="/index.css" rel="stylesheet" />
|
|
8
|
+
<link
|
|
9
|
+
href="/node_modules/@speed-highlight/core/dist/themes/default.css"
|
|
10
|
+
media="(prefers-color-scheme: light)"
|
|
11
|
+
rel="stylesheet"
|
|
12
|
+
/>
|
|
13
|
+
<link
|
|
14
|
+
href="/node_modules/@speed-highlight/core/dist/themes/atom-dark.css"
|
|
15
|
+
media="(prefers-color-scheme: dark)"
|
|
16
|
+
rel="stylesheet"
|
|
17
|
+
/>
|
|
18
|
+
<!--app-head-->
|
|
19
|
+
</head>
|
|
20
|
+
<body>
|
|
21
|
+
<div id="app"><!--app-html--></div>
|
|
22
|
+
<script prerender type="module" src="/src/app.js"></script>
|
|
23
|
+
</body>
|
|
24
|
+
</html>
|
package/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { listen, production } from "evikit/server";
|
|
2
|
+
import express from "express";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import swaggerUi from "swagger-ui-express";
|
|
5
|
+
|
|
6
|
+
import { api } from "./src/api.js";
|
|
7
|
+
import { prerender } from "./src/app.js";
|
|
8
|
+
|
|
9
|
+
const app = express();
|
|
10
|
+
app.use(api().onRequest);
|
|
11
|
+
app.use("/openapi", swaggerUi.serve, swaggerUi.setup(api().openapi));
|
|
12
|
+
app.use("/", express.static("./dist", { index: false, redirect: false }));
|
|
13
|
+
app.use(
|
|
14
|
+
production({
|
|
15
|
+
html: await readFile("./dist/index.html", "utf-8"),
|
|
16
|
+
prerender,
|
|
17
|
+
}),
|
|
18
|
+
);
|
|
19
|
+
listen(app);
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-evikit",
|
|
3
|
+
"version": "2.2.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": "bin/create-evikit.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite dev",
|
|
8
|
+
"build": "tsc && vite build",
|
|
9
|
+
"start": "cross-env NODE_ENV=production node --env-file=.env index.js",
|
|
10
|
+
"extract": "evikit-extract",
|
|
11
|
+
"format": "prettier --write . && eslint --fix .",
|
|
12
|
+
"lint": "prettier --check . && eslint ."
|
|
13
|
+
},
|
|
14
|
+
"author": "Denys Nykula <nykula@ukr.net>",
|
|
15
|
+
"license": "0BSD",
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@eslint/compat": "^2.1.0",
|
|
18
|
+
"@eslint/js": "^10.0.1",
|
|
19
|
+
"@preact/preset-vite": "^2.10.5",
|
|
20
|
+
"@tailwindcss/vite": "^4.3.2",
|
|
21
|
+
"@types/express": "^5.0.6",
|
|
22
|
+
"@types/node": "^26.1.0",
|
|
23
|
+
"@types/swagger-ui-express": "^4.1.8",
|
|
24
|
+
"cross-env": "^10.1.0",
|
|
25
|
+
"daisyui": "^5.6.14",
|
|
26
|
+
"eslint": "^10.6.0",
|
|
27
|
+
"eslint-config-prettier": "^10.1.8",
|
|
28
|
+
"eslint-plugin-perfectionist": "^5.10.0",
|
|
29
|
+
"globals": "^17.7.0",
|
|
30
|
+
"prettier": "^3.9.4",
|
|
31
|
+
"typescript": "^6.0.3",
|
|
32
|
+
"typescript-eslint": "^8.63.0",
|
|
33
|
+
"vite": "^8.1.3",
|
|
34
|
+
"vite-bundle-analyzer": "^1.3.8"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@speed-highlight/core": "^1.2.17",
|
|
38
|
+
"accept-language": "^3.0.20",
|
|
39
|
+
"classnames": "^2.5.1",
|
|
40
|
+
"evikit": "^2.2.2",
|
|
41
|
+
"express": "^5.2.1",
|
|
42
|
+
"file-type": "^22.0.1",
|
|
43
|
+
"hoofd": "^1.7.3",
|
|
44
|
+
"preact": "^10.29.5",
|
|
45
|
+
"preact-iso": "^2.12.0",
|
|
46
|
+
"swagger-ui-express": "^5.0.1",
|
|
47
|
+
"valibot": "^1.4.2"
|
|
48
|
+
},
|
|
49
|
+
"allowScripts": {
|
|
50
|
+
"@scarf/scarf": false
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# SOME DESCRIPTIVE TITLE.
|
|
2
|
+
# Copyright (C) YEAR Denys Nykula <nykula@ukr.net>
|
|
3
|
+
# This file is distributed under the same license as the create-evikit package.
|
|
4
|
+
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
|
|
5
|
+
#
|
|
6
|
+
#, fuzzy
|
|
7
|
+
msgid ""
|
|
8
|
+
msgstr ""
|
|
9
|
+
"Project-Id-Version: create-evikit\n"
|
|
10
|
+
"Report-Msgid-Bugs-To: \n"
|
|
11
|
+
"POT-Creation-Date: 2026-07-07 23:06+0300\n"
|
|
12
|
+
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
|
13
|
+
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
|
14
|
+
"Language-Team: LANGUAGE <LL@li.org>\n"
|
|
15
|
+
"Language: \n"
|
|
16
|
+
"MIME-Version: 1.0\n"
|
|
17
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
18
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
19
|
+
"Plural-Forms: nplurals=INTEGER; plural=EXPRESSION;\n"
|
|
20
|
+
|
|
21
|
+
msgid "EviKit: Preact SSR framework for small web apps"
|
|
22
|
+
msgstr ""
|
|
23
|
+
|
|
24
|
+
msgid "Preact SSR framework."
|
|
25
|
+
msgstr ""
|
|
26
|
+
|
|
27
|
+
msgid "For small web apps hosted on a <b>VPS</b> or in local network."
|
|
28
|
+
msgstr ""
|
|
29
|
+
|
|
30
|
+
msgid ""
|
|
31
|
+
"Standard modern <b>JavaScript</b>, no JSX or TS. <b>Forms</b> accessible "
|
|
32
|
+
"when JS doesn't load. Automatic <b>Swagger</b> specification. First-class "
|
|
33
|
+
"API input/output <b>validation</b>. Built-in <b>node:sqlite ORM</b>. Support "
|
|
34
|
+
"for UI <b>translation</b> through .po files."
|
|
35
|
+
msgstr ""
|
|
36
|
+
|
|
37
|
+
msgid "Documentation"
|
|
38
|
+
msgstr ""
|
|
39
|
+
|
|
40
|
+
msgid "Report <b>issue</b>"
|
|
41
|
+
msgstr ""
|
|
42
|
+
|
|
43
|
+
msgid ""
|
|
44
|
+
"See <a href=\"{todo}\">live demo</a> of the example code from below. Todos "
|
|
45
|
+
"are deleted every 10 minutes."
|
|
46
|
+
msgstr ""
|
|
47
|
+
|
|
48
|
+
msgid "Quick start"
|
|
49
|
+
msgstr ""
|
|
50
|
+
|
|
51
|
+
msgid ""
|
|
52
|
+
"There’s intentionally <b>not much API</b>. Documentation entirely fits in a "
|
|
53
|
+
"long but straightforward <b>README</b>. Like the Django tutorial, it guides "
|
|
54
|
+
"you through <b><a href=\"{documentation}\">writing an application</a></b> "
|
|
55
|
+
"using all the primary features of the EviKit framework."
|
|
56
|
+
msgstr ""
|
|
57
|
+
|
|
58
|
+
msgid ""
|
|
59
|
+
"<a href=\"{evikit}\"><b>Source code</b> at Codeberg</a> under <b>MIT</b> "
|
|
60
|
+
"license"
|
|
61
|
+
msgstr ""
|
|
62
|
+
|
|
63
|
+
msgid "Discuss on <b>Fediverse</b>"
|
|
64
|
+
msgstr ""
|
|
65
|
+
|
|
66
|
+
msgid "%1 todo"
|
|
67
|
+
msgid_plural "%1 todos"
|
|
68
|
+
msgstr[0] ""
|
|
69
|
+
msgstr[1] ""
|
|
70
|
+
|
|
71
|
+
msgid "Todos"
|
|
72
|
+
msgstr ""
|
|
73
|
+
|
|
74
|
+
msgid "Text"
|
|
75
|
+
msgstr ""
|
|
76
|
+
|
|
77
|
+
msgid "Create todo"
|
|
78
|
+
msgstr ""
|
|
79
|
+
|
|
80
|
+
msgid "Back to plan"
|
|
81
|
+
msgstr ""
|
|
82
|
+
|
|
83
|
+
msgid "Done"
|
|
84
|
+
msgstr ""
|
package/po/uk.po
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Ukrainian translations for create-evikit package.
|
|
2
|
+
# Copyright (C) 2026 Denys Nykula <nykula@ukr.net>
|
|
3
|
+
# This file is distributed under the same license as the create-evikit package.
|
|
4
|
+
# Denys Nykula <nykula@ukr.net>, 2026.
|
|
5
|
+
#
|
|
6
|
+
msgid ""
|
|
7
|
+
msgstr ""
|
|
8
|
+
"Project-Id-Version: create-evikit\n"
|
|
9
|
+
"Report-Msgid-Bugs-To: \n"
|
|
10
|
+
"POT-Creation-Date: 2026-07-07 23:06+0300\n"
|
|
11
|
+
"PO-Revision-Date: 2026-07-07 23:07+0300\n"
|
|
12
|
+
"Last-Translator: EMAIL@ADDRESS\n"
|
|
13
|
+
"Language-Team: Ukrainian\n"
|
|
14
|
+
"Language: uk\n"
|
|
15
|
+
"MIME-Version: 1.0\n"
|
|
16
|
+
"Content-Type: text/plain; charset=UTF-8\n"
|
|
17
|
+
"Content-Transfer-Encoding: 8bit\n"
|
|
18
|
+
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
|
|
19
|
+
"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);\n"
|
|
20
|
+
"X-Generator: Poedit 3.9\n"
|
|
21
|
+
|
|
22
|
+
msgid "EviKit: Preact SSR framework for small web apps"
|
|
23
|
+
msgstr "EviKit: Preact SSR-фреймворк для невеликих вебзастосунків"
|
|
24
|
+
|
|
25
|
+
msgid "Preact SSR framework."
|
|
26
|
+
msgstr "Preact SSR-фреймворк."
|
|
27
|
+
|
|
28
|
+
msgid "For small web apps hosted on a <b>VPS</b> or in local network."
|
|
29
|
+
msgstr ""
|
|
30
|
+
"Для невеликих вебзастосунків, розміщених на <b>VPS</b> чи в локальній мережі."
|
|
31
|
+
|
|
32
|
+
msgid ""
|
|
33
|
+
"Standard modern <b>JavaScript</b>, no JSX or TS. <b>Forms</b> accessible "
|
|
34
|
+
"when JS doesn't load. Automatic <b>Swagger</b> specification. First-class "
|
|
35
|
+
"API input/output <b>validation</b>. Built-in <b>node:sqlite ORM</b>. Support "
|
|
36
|
+
"for UI <b>translation</b> through .po files."
|
|
37
|
+
msgstr ""
|
|
38
|
+
"Стандартний сучасний <b>JavaScript</b>, без JSX чи TS. <b>Форми</b> "
|
|
39
|
+
"доступні, навіть коли JS не запускається. <b>Swagger</b>-специфікація "
|
|
40
|
+
"генерується автоматично. <b>Валідація</b> введення-виведення API - з "
|
|
41
|
+
"коробки. Вбудована <b>ORM</b> на основі <b>node:sqlite</b>. Підтримується "
|
|
42
|
+
"<b>переклад</b> інтерфейсу через .po-файли."
|
|
43
|
+
|
|
44
|
+
msgid "Documentation"
|
|
45
|
+
msgstr "Документація"
|
|
46
|
+
|
|
47
|
+
msgid "Report <b>issue</b>"
|
|
48
|
+
msgstr "Повідомити про <b>ваду</b>"
|
|
49
|
+
|
|
50
|
+
msgid ""
|
|
51
|
+
"See <a href=\"{todo}\">live demo</a> of the example code from below. Todos "
|
|
52
|
+
"are deleted every 10 minutes."
|
|
53
|
+
msgstr ""
|
|
54
|
+
"Перегляньте <a href=\"{todo}\">живе демо</a> коду зразків, наведених унизу. "
|
|
55
|
+
"Завдання видаляються кожні 10 хвилин."
|
|
56
|
+
|
|
57
|
+
msgid "Quick start"
|
|
58
|
+
msgstr "Швидкий початок"
|
|
59
|
+
|
|
60
|
+
msgid ""
|
|
61
|
+
"There’s intentionally <b>not much API</b>. Documentation entirely fits in a "
|
|
62
|
+
"long but straightforward <b>README</b>. Like the Django tutorial, it guides "
|
|
63
|
+
"you through <b><a href=\"{documentation}\">writing an application</a></b> "
|
|
64
|
+
"using all the primary features of the EviKit framework."
|
|
65
|
+
msgstr ""
|
|
66
|
+
"<b>API навмисне небагато</b>. Документація повністю вміщається в довгий, але "
|
|
67
|
+
"послідовний <b>README</b>-файл. Подібно до посібника Django, він містить "
|
|
68
|
+
"покрокові інструкції з <b><a href=\"{documentation}\">написання застосунка</"
|
|
69
|
+
"a></b>, що використовує всі основні функції фреймворка EviKit."
|
|
70
|
+
|
|
71
|
+
msgid ""
|
|
72
|
+
"<a href=\"{evikit}\"><b>Source code</b> at Codeberg</a> under <b>MIT</b> "
|
|
73
|
+
"license"
|
|
74
|
+
msgstr ""
|
|
75
|
+
"<a href=\"{evikit}\"><b>Вихідний код</b> на Codeberg</a> на умовах ліцензії "
|
|
76
|
+
"<b>MIT</b>"
|
|
77
|
+
|
|
78
|
+
msgid "Discuss on <b>Fediverse</b>"
|
|
79
|
+
msgstr "Обговорити в <b>Fediverse</b>"
|
|
80
|
+
|
|
81
|
+
msgid "%1 todo"
|
|
82
|
+
msgid_plural "%1 todos"
|
|
83
|
+
msgstr[0] "%1 завдання"
|
|
84
|
+
msgstr[1] "%1 завдання"
|
|
85
|
+
msgstr[2] "%1 завдань"
|
|
86
|
+
|
|
87
|
+
msgid "Todos"
|
|
88
|
+
msgstr "Завдання"
|
|
89
|
+
|
|
90
|
+
msgid "Text"
|
|
91
|
+
msgstr "Текст"
|
|
92
|
+
|
|
93
|
+
msgid "Create todo"
|
|
94
|
+
msgstr "Створити завдання"
|
|
95
|
+
|
|
96
|
+
msgid "Back to plan"
|
|
97
|
+
msgstr "Назад до плану"
|
|
98
|
+
|
|
99
|
+
msgid "Done"
|
|
100
|
+
msgstr "Готово"
|
package/src/api.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { route, router } from "evikit/server";
|
|
2
|
+
|
|
3
|
+
import pkg from "../package.json" with { type: "json" };
|
|
4
|
+
import { acceptLanguages } from "./lib/index.js";
|
|
5
|
+
import { HomeServer } from "./routes/api/server.js";
|
|
6
|
+
import { TodoServer } from "./routes/api/todo/server.js";
|
|
7
|
+
|
|
8
|
+
acceptLanguages();
|
|
9
|
+
|
|
10
|
+
export function api() {
|
|
11
|
+
return router(
|
|
12
|
+
{ pkg },
|
|
13
|
+
route("/api", HomeServer),
|
|
14
|
+
route("/api/todo", TodoServer),
|
|
15
|
+
);
|
|
16
|
+
}
|
package/src/app.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { hydrate, Router, ssr } from "evikit";
|
|
2
|
+
import { h } from "preact";
|
|
3
|
+
import { Route } from "preact-iso";
|
|
4
|
+
|
|
5
|
+
import { acceptLanguages } from "./lib/index.js";
|
|
6
|
+
import { HomePage } from "./routes/page.js";
|
|
7
|
+
import { TodoPage } from "./routes/todo/page.js";
|
|
8
|
+
|
|
9
|
+
acceptLanguages();
|
|
10
|
+
|
|
11
|
+
/** @param {any} data */
|
|
12
|
+
export const prerender = (data) => ssr(App, data);
|
|
13
|
+
|
|
14
|
+
/** @param {{ dehydratedState?: any, i18n: any }} props */
|
|
15
|
+
export function App(props) {
|
|
16
|
+
return h(
|
|
17
|
+
Router,
|
|
18
|
+
props,
|
|
19
|
+
h(Route, { component: HomePage, path: "/" }),
|
|
20
|
+
h(Route, { component: TodoPage, path: "/todo" }),
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
hydrate(App, "#app");
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { sqlite } from "evikit/sqlite";
|
|
2
|
+
|
|
3
|
+
import pkg from "../../../package.json" with { type: "json" };
|
|
4
|
+
import * as schema from "./schema.js";
|
|
5
|
+
|
|
6
|
+
export const db = sqlite({ pkg, schema });
|
|
7
|
+
|
|
8
|
+
if (
|
|
9
|
+
process.env["npm_command"] == "start" ||
|
|
10
|
+
process.env["npm_lifecycle_event"] == "dev"
|
|
11
|
+
) {
|
|
12
|
+
setInterval(() => {
|
|
13
|
+
db.Todo.delete({
|
|
14
|
+
text: db.Todo.select()
|
|
15
|
+
.filter(({ createdAt }) => createdAt < Date.now() - 10 * 60e3)
|
|
16
|
+
.map(({ text }) => text),
|
|
17
|
+
});
|
|
18
|
+
}, 1000);
|
|
19
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { primary } from "evikit";
|
|
2
|
+
import { integer, number, object, optional, pipe, string } from "valibot";
|
|
3
|
+
|
|
4
|
+
export const Todo = object({
|
|
5
|
+
createdAt: pipe(number(), integer()),
|
|
6
|
+
doneAt: optional(pipe(number(), integer())),
|
|
7
|
+
text: pipe(string(), primary()),
|
|
8
|
+
});
|
package/src/lib/index.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { endpoint, json } from "evikit/server";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
|
|
4
|
+
import { HomeGet } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const template = [
|
|
7
|
+
"src/lib/db/schema.js",
|
|
8
|
+
"src/routes/api/todo/types.js",
|
|
9
|
+
"src/routes/api/todo/server.js",
|
|
10
|
+
"src/routes/todo/page.js",
|
|
11
|
+
"src/api.js",
|
|
12
|
+
"src/app.js",
|
|
13
|
+
"index.js",
|
|
14
|
+
"vite.config.js",
|
|
15
|
+
"src/lib/db/index.js",
|
|
16
|
+
"index.html",
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
export const HomeServer = {
|
|
20
|
+
get: endpoint(HomeGet, async () =>
|
|
21
|
+
json({
|
|
22
|
+
File: Object.fromEntries(
|
|
23
|
+
await Promise.all(
|
|
24
|
+
template.map(async (path) => [path, await readFile(path, "utf8")]),
|
|
25
|
+
),
|
|
26
|
+
),
|
|
27
|
+
}),
|
|
28
|
+
),
|
|
29
|
+
};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { endpoint, json, redirect } from "evikit/server";
|
|
2
|
+
|
|
3
|
+
import { db } from "../../../lib/db/index.js";
|
|
4
|
+
import { TodoGet, TodoPost } from "./types.js";
|
|
5
|
+
|
|
6
|
+
export const TodoServer = {
|
|
7
|
+
get: endpoint(TodoGet, async () => json({ Todo: db.Todo.select() })),
|
|
8
|
+
|
|
9
|
+
post: endpoint(TodoPost, async ({ input }) => {
|
|
10
|
+
const todo = db.Todo.select().find(({ text }) => text == input.text);
|
|
11
|
+
if (todo) {
|
|
12
|
+
db.Todo.update({
|
|
13
|
+
set: { doneAt: todo.doneAt ? undefined : Date.now() },
|
|
14
|
+
where: { text: todo.text },
|
|
15
|
+
});
|
|
16
|
+
} else {
|
|
17
|
+
db.Todo.upsert({ createdAt: Date.now(), text: input.text });
|
|
18
|
+
}
|
|
19
|
+
return redirect("/todo", 302);
|
|
20
|
+
}),
|
|
21
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { array, object, string } from "valibot";
|
|
2
|
+
|
|
3
|
+
import * as schema from "../../../lib/db/schema.js";
|
|
4
|
+
|
|
5
|
+
export const TodoGet = {
|
|
6
|
+
input: object({ lang: string() }),
|
|
7
|
+
outputs: { 200: object({ Todo: array(schema.Todo) }) },
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export const TodoPost = {
|
|
11
|
+
input: object({ lang: string(), text: string() }),
|
|
12
|
+
outputs: { 302: string(), 400: string() },
|
|
13
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { highlightAll } from "@speed-highlight/core";
|
|
2
|
+
import { a, b, div, figure, h1, main, p, span, useApi, useI18n } from "evikit";
|
|
3
|
+
import { useMeta, useTitle } from "hoofd/preact";
|
|
4
|
+
import { h } from "preact";
|
|
5
|
+
import { useEffect } from "preact/hooks";
|
|
6
|
+
|
|
7
|
+
import { HomeGet } from "./api/types.js";
|
|
8
|
+
|
|
9
|
+
export function HomePage() {
|
|
10
|
+
const i18n = useI18n();
|
|
11
|
+
const { data } = useApi(HomeGet);
|
|
12
|
+
useTitle(i18n.gettext("EviKit: Preact SSR framework for small web apps"));
|
|
13
|
+
useMeta({
|
|
14
|
+
content:
|
|
15
|
+
"Standard modern JavaScript, no JSX or TS. Forms accessible when JS doesn't load. Automatic Swagger specification. First-class API input/output validation. Built-in node:sqlite ORM. Support for UI translation through .po files.",
|
|
16
|
+
name: "description",
|
|
17
|
+
});
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
highlightAll();
|
|
20
|
+
}, [JSON.stringify(data?.File)]);
|
|
21
|
+
|
|
22
|
+
return main(
|
|
23
|
+
{ class: "max-w-5xl mx-auto p-8" },
|
|
24
|
+
h1(
|
|
25
|
+
{ class: "mb-4 text-xl lg:text-3xl lg:text-center" },
|
|
26
|
+
"EviKit: ",
|
|
27
|
+
b(null, i18n.gettext("Preact SSR framework.")),
|
|
28
|
+
h("br", null),
|
|
29
|
+
span({
|
|
30
|
+
class: "mt-0 text-base lg:text-sm",
|
|
31
|
+
...sanitize(
|
|
32
|
+
i18n.gettext(
|
|
33
|
+
"For small web apps hosted on a <b>VPS</b> or in local network.",
|
|
34
|
+
),
|
|
35
|
+
),
|
|
36
|
+
}),
|
|
37
|
+
),
|
|
38
|
+
|
|
39
|
+
p({
|
|
40
|
+
class: "lg:text-center",
|
|
41
|
+
...sanitize(
|
|
42
|
+
i18n.gettext(
|
|
43
|
+
"Standard modern <b>JavaScript</b>, no JSX or TS. <b>Forms</b> accessible when JS doesn't load. Automatic <b>Swagger</b> specification. First-class API input/output <b>validation</b>. Built-in <b>node:sqlite ORM</b>. Support for UI <b>translation</b> through .po files.",
|
|
44
|
+
),
|
|
45
|
+
),
|
|
46
|
+
}),
|
|
47
|
+
|
|
48
|
+
h(
|
|
49
|
+
"nav",
|
|
50
|
+
{ class: "gap-4 grid grid-cols-2 max-w-lg mx-auto mt-6 text-center" },
|
|
51
|
+
|
|
52
|
+
a(
|
|
53
|
+
{ href: "https://codeberg.org/nykula/evikit#documentation" },
|
|
54
|
+
b(null, i18n.gettext("Documentation")),
|
|
55
|
+
" (v2)",
|
|
56
|
+
),
|
|
57
|
+
|
|
58
|
+
a({
|
|
59
|
+
href: "https://codeberg.org/nykula/evikit/issues",
|
|
60
|
+
...sanitize(i18n.gettext("Report <b>issue</b>")),
|
|
61
|
+
}),
|
|
62
|
+
),
|
|
63
|
+
|
|
64
|
+
p({
|
|
65
|
+
class: "mb-6 lg:text-center",
|
|
66
|
+
...sanitize(
|
|
67
|
+
i18n
|
|
68
|
+
.gettext(
|
|
69
|
+
'See <a href="{todo}">live demo</a> of the example code from below. Todos are deleted every 10 minutes.',
|
|
70
|
+
)
|
|
71
|
+
.replace("{todo}", "/todo"),
|
|
72
|
+
),
|
|
73
|
+
}),
|
|
74
|
+
|
|
75
|
+
h(
|
|
76
|
+
"blockquote",
|
|
77
|
+
{
|
|
78
|
+
class: "max-w-xs mx-auto mb-8 p-4 rounded-lg text-center",
|
|
79
|
+
style: {
|
|
80
|
+
background: "rgba(0, 0, 0, 0.025)",
|
|
81
|
+
border: "1px solid rgba(0, 0, 0, 0.05)",
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
p(null, b(null, i18n.gettext("Quick start"))),
|
|
85
|
+
h(
|
|
86
|
+
"pre",
|
|
87
|
+
null,
|
|
88
|
+
h(
|
|
89
|
+
"code",
|
|
90
|
+
{ class: "!bg-transparent shj-lang-bash" },
|
|
91
|
+
`
|
|
92
|
+
npm init evikit my-app
|
|
93
|
+
cd my-app
|
|
94
|
+
npm run dev
|
|
95
|
+
`.trimStart(),
|
|
96
|
+
),
|
|
97
|
+
),
|
|
98
|
+
),
|
|
99
|
+
|
|
100
|
+
div(
|
|
101
|
+
{ class: "grid grid-cols-1 lg:grid-cols-2 gap-8 w-full" },
|
|
102
|
+
h(
|
|
103
|
+
Code,
|
|
104
|
+
{ file: data?.File["src/lib/db/schema.js"], lang: "js" },
|
|
105
|
+
span(null, "src/lib/"),
|
|
106
|
+
b(null, "db/schema.js"),
|
|
107
|
+
),
|
|
108
|
+
|
|
109
|
+
h(
|
|
110
|
+
Code,
|
|
111
|
+
{ file: data?.File["src/routes/api/todo/types.js"], lang: "js" },
|
|
112
|
+
span(null, "src/routes/"),
|
|
113
|
+
b(null, "api/todo/types.js"),
|
|
114
|
+
),
|
|
115
|
+
|
|
116
|
+
h(
|
|
117
|
+
Code,
|
|
118
|
+
{ file: data?.File["src/routes/api/todo/server.js"], lang: "js" },
|
|
119
|
+
span(null, "src/routes/"),
|
|
120
|
+
b(null, "api/todo/server.js"),
|
|
121
|
+
),
|
|
122
|
+
|
|
123
|
+
h(
|
|
124
|
+
Code,
|
|
125
|
+
{ file: data?.File["src/routes/todo/page.js"], lang: "js" },
|
|
126
|
+
span(null, "src/routes/"),
|
|
127
|
+
b(null, "todo/page.js"),
|
|
128
|
+
),
|
|
129
|
+
|
|
130
|
+
h(
|
|
131
|
+
Code,
|
|
132
|
+
{ file: data?.File["src/api.js"], lang: "js", tag: "details" },
|
|
133
|
+
span(null, "src/"),
|
|
134
|
+
b(null, "api.js"),
|
|
135
|
+
),
|
|
136
|
+
|
|
137
|
+
h(
|
|
138
|
+
Code,
|
|
139
|
+
{ file: data?.File["src/app.js"], lang: "js", tag: "details" },
|
|
140
|
+
span(null, "src/"),
|
|
141
|
+
b(null, "app.js"),
|
|
142
|
+
),
|
|
143
|
+
|
|
144
|
+
h(
|
|
145
|
+
Code,
|
|
146
|
+
{ file: data?.File["index.js"], lang: "js", tag: "details" },
|
|
147
|
+
b(null, "index.js"),
|
|
148
|
+
),
|
|
149
|
+
|
|
150
|
+
h(
|
|
151
|
+
Code,
|
|
152
|
+
{ file: data?.File["vite.config.js"], lang: "js", tag: "details" },
|
|
153
|
+
b(null, "vite.config.js"),
|
|
154
|
+
),
|
|
155
|
+
|
|
156
|
+
h(
|
|
157
|
+
Code,
|
|
158
|
+
{ file: data?.File["src/lib/db/index.js"], lang: "js", tag: "details" },
|
|
159
|
+
span(null, "src/lib/"),
|
|
160
|
+
b(null, "db/index.js"),
|
|
161
|
+
),
|
|
162
|
+
|
|
163
|
+
h(
|
|
164
|
+
Code,
|
|
165
|
+
{ file: data?.File["index.html"], lang: "html", tag: "details" },
|
|
166
|
+
b(null, "index.html"),
|
|
167
|
+
),
|
|
168
|
+
),
|
|
169
|
+
|
|
170
|
+
h("hr", { class: "opacity-0" }),
|
|
171
|
+
h(
|
|
172
|
+
"blockquote",
|
|
173
|
+
{
|
|
174
|
+
class: "max-w-2xl mx-auto my-8 p-4 rounded-lg text-center",
|
|
175
|
+
style: {
|
|
176
|
+
background: "rgba(0, 0, 0, 0.025)",
|
|
177
|
+
border: "1px solid rgba(0, 0, 0, 0.05)",
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
p({
|
|
181
|
+
...sanitize(
|
|
182
|
+
i18n
|
|
183
|
+
.gettext(
|
|
184
|
+
'There’s intentionally <b>not much API</b>. Documentation entirely fits in a long but straightforward <b>README</b>. Like the Django tutorial, it guides you through <b><a href="{documentation}">writing an application</a></b> using all the primary features of the EviKit framework.',
|
|
185
|
+
)
|
|
186
|
+
.replace(
|
|
187
|
+
"{documentation}",
|
|
188
|
+
"https://codeberg.org/nykula/evikit#documentation",
|
|
189
|
+
),
|
|
190
|
+
),
|
|
191
|
+
}),
|
|
192
|
+
),
|
|
193
|
+
|
|
194
|
+
h("hr", { class: "opacity-0" }),
|
|
195
|
+
h(
|
|
196
|
+
"footer",
|
|
197
|
+
{ class: "text-center" },
|
|
198
|
+
p(
|
|
199
|
+
{ class: "mb-4" },
|
|
200
|
+
"Copyright © 2026 ",
|
|
201
|
+
a({ href: "https://libre.net.ua" }, "Denys Nykula"),
|
|
202
|
+
),
|
|
203
|
+
p({
|
|
204
|
+
class: "mb-4",
|
|
205
|
+
...sanitize(
|
|
206
|
+
i18n
|
|
207
|
+
.gettext(
|
|
208
|
+
'<a href="{evikit}"><b>Source code</b> at Codeberg</a> under <b>MIT</b> license',
|
|
209
|
+
)
|
|
210
|
+
.replace("{evikit}", "https://codeberg.org/nykula/evikit"),
|
|
211
|
+
),
|
|
212
|
+
}),
|
|
213
|
+
p(
|
|
214
|
+
{ class: "mb-4" },
|
|
215
|
+
a({
|
|
216
|
+
href: "https://programming.dev/post/52844785",
|
|
217
|
+
...sanitize(i18n.gettext("Discuss on <b>Fediverse</b>")),
|
|
218
|
+
}),
|
|
219
|
+
),
|
|
220
|
+
),
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** @param {{ children?: any, lang: string, file: string | undefined, tag?: string }} props */
|
|
225
|
+
function Code({ children, file, lang, tag }) {
|
|
226
|
+
const inner = h(
|
|
227
|
+
"pre",
|
|
228
|
+
{ class: "mt-4 overflow-auto text-sm" },
|
|
229
|
+
h(
|
|
230
|
+
"code",
|
|
231
|
+
{ class: `w-full shj-lang-${lang}` },
|
|
232
|
+
file
|
|
233
|
+
?.replace(/.*EviKit.*\r?\n/g, "")
|
|
234
|
+
.replace(/\{[\s\S]*?\} from/g, (match) => match.replace(/\s+/g, " "))
|
|
235
|
+
.replace(/.*(HomeServer|acceptLanguage).*\r?\n/g, "")
|
|
236
|
+
.replace(/.*(HomePage|acceptLanguage).*\r?\n/g, "")
|
|
237
|
+
.replace(
|
|
238
|
+
/<link\s+href="\/node_modules\/[^"]+.css"\s+media="[^"]+"\s+rel="stylesheet"\s+\/>\s+/g,
|
|
239
|
+
"",
|
|
240
|
+
),
|
|
241
|
+
),
|
|
242
|
+
);
|
|
243
|
+
const style = "border-lg mb-4 mx-0 overflow-hidden p-4";
|
|
244
|
+
return tag == "details"
|
|
245
|
+
? h("details", { class: style }, h("summary", null, children), inner)
|
|
246
|
+
: figure({ class: style }, h("figcaption", null, children), inner);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/** @param {string} html */
|
|
250
|
+
function sanitize(html) {
|
|
251
|
+
return {
|
|
252
|
+
dangerouslySetInnerHTML: {
|
|
253
|
+
__html: html.replace(/<([^>]*)>/g, (match) =>
|
|
254
|
+
/^<\/?[a-z]+>$/.test(match) || /^<a href="[a-z:/.#]*">$/.test(match)
|
|
255
|
+
? match
|
|
256
|
+
: "",
|
|
257
|
+
),
|
|
258
|
+
},
|
|
259
|
+
};
|
|
260
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import classNames from "classnames";
|
|
2
|
+
import {
|
|
3
|
+
a,
|
|
4
|
+
button,
|
|
5
|
+
form,
|
|
6
|
+
input,
|
|
7
|
+
main,
|
|
8
|
+
p,
|
|
9
|
+
useApi,
|
|
10
|
+
useEnhance,
|
|
11
|
+
useI18n,
|
|
12
|
+
} from "evikit";
|
|
13
|
+
import { useMeta, useTitle } from "hoofd/preact";
|
|
14
|
+
|
|
15
|
+
import { TodoGet, TodoPost } from "../api/todo/types.js";
|
|
16
|
+
|
|
17
|
+
export function TodoPage() {
|
|
18
|
+
const { data } = useApi(TodoGet);
|
|
19
|
+
const { busy, onSubmit } = useEnhance(TodoPost);
|
|
20
|
+
const i18n = useI18n();
|
|
21
|
+
useTitle(
|
|
22
|
+
data?.Todo.length
|
|
23
|
+
? i18n.ngettext("%1 todo", "%1 todos", data.Todo.length, data.Todo.length)
|
|
24
|
+
: i18n.gettext("Todos"),
|
|
25
|
+
);
|
|
26
|
+
useMeta({ content: "noindex", name: "robots" });
|
|
27
|
+
|
|
28
|
+
return main(
|
|
29
|
+
{ class: "m-8" },
|
|
30
|
+
form(
|
|
31
|
+
{ action: "/api/todo", method: "POST", onSubmit },
|
|
32
|
+
input({
|
|
33
|
+
class: "input",
|
|
34
|
+
name: "text",
|
|
35
|
+
placeholder: i18n.gettext("Text"),
|
|
36
|
+
required: true,
|
|
37
|
+
}),
|
|
38
|
+
button(
|
|
39
|
+
{ class: classNames("btn", busy && "btn-disabled") },
|
|
40
|
+
i18n.gettext("Create todo"),
|
|
41
|
+
),
|
|
42
|
+
),
|
|
43
|
+
(data?.Todo || []).map(({ doneAt, text }) =>
|
|
44
|
+
form(
|
|
45
|
+
{ action: "/api/todo", method: "POST", onSubmit },
|
|
46
|
+
button(
|
|
47
|
+
{ class: classNames("btn", busy && "btn-disabled") },
|
|
48
|
+
doneAt ? i18n.gettext("Back to plan") : i18n.gettext("Done"),
|
|
49
|
+
),
|
|
50
|
+
input({ class: "input", name: "text", readOnly: true, value: text }),
|
|
51
|
+
),
|
|
52
|
+
),
|
|
53
|
+
p({ class: "mt-4" }, a({ href: "/" }, "EviKit")),
|
|
54
|
+
);
|
|
55
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"outDir": "dist",
|
|
4
|
+
"module": "nodenext",
|
|
5
|
+
"target": "esnext",
|
|
6
|
+
"types": [],
|
|
7
|
+
"sourceMap": true,
|
|
8
|
+
"declaration": true,
|
|
9
|
+
"declarationMap": true,
|
|
10
|
+
"noUncheckedIndexedAccess": true,
|
|
11
|
+
"exactOptionalPropertyTypes": true,
|
|
12
|
+
"noImplicitReturns": true,
|
|
13
|
+
"noImplicitOverride": true,
|
|
14
|
+
"noUnusedLocals": true,
|
|
15
|
+
"noUnusedParameters": true,
|
|
16
|
+
"noFallthroughCasesInSwitch": true,
|
|
17
|
+
"noPropertyAccessFromIndexSignature": true,
|
|
18
|
+
"strict": true,
|
|
19
|
+
"jsx": "react-jsx",
|
|
20
|
+
"verbatimModuleSyntax": true,
|
|
21
|
+
"isolatedModules": true,
|
|
22
|
+
"noUncheckedSideEffectImports": true,
|
|
23
|
+
"moduleDetection": "force",
|
|
24
|
+
"skipLibCheck": true,
|
|
25
|
+
"allowJs": true,
|
|
26
|
+
"checkJs": true,
|
|
27
|
+
"noEmit": true,
|
|
28
|
+
"esModuleInterop": true
|
|
29
|
+
}
|
|
30
|
+
}
|
package/vite.config.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import preact from "@preact/preset-vite";
|
|
2
|
+
import tailwindcss from "@tailwindcss/vite";
|
|
3
|
+
import { defineConfig } from "vite";
|
|
4
|
+
import { analyzer } from "vite-bundle-analyzer";
|
|
5
|
+
|
|
6
|
+
import { api } from "./src/api.js";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
plugins: [
|
|
10
|
+
analyzer({ analyzerMode: "static" }),
|
|
11
|
+
{
|
|
12
|
+
configureServer(server) {
|
|
13
|
+
server.middlewares.use(api().onRequest);
|
|
14
|
+
},
|
|
15
|
+
name: "configure-server",
|
|
16
|
+
},
|
|
17
|
+
preact({ prerender: { enabled: true, renderTarget: "#app" } }),
|
|
18
|
+
tailwindcss(),
|
|
19
|
+
],
|
|
20
|
+
});
|