create-tinyjoin 0.0.5 → 0.0.6
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/README.md +52 -4
- package/cli.js +4 -4
- package/package.json +1 -1
- package/templates/AGENTS.md.hbs +49 -12
- package/templates/README.md.hbs +40 -15
- package/templates/client/index.html.hbs +51 -45
- package/templates/client/package.json.hbs +3 -1
- package/templates/client/public/favicon.svg +6 -0
- package/templates/client/public/js.svg +5 -0
- package/templates/client/public/ts.svg +6 -0
- package/templates/client/src/app.ts.hbs +28 -0
- package/templates/client/src/button.css.hbs +25 -0
- package/templates/client/src/button.ts.hbs +19 -0
- package/templates/client/src/database.ts.hbs +50 -23
- package/templates/client/src/index.ts.hbs +7 -0
- package/templates/client/src/info.css.hbs +93 -0
- package/templates/client/src/info.ts.hbs +42 -0
- package/templates/client/src/input.css.hbs +22 -0
- package/templates/client/src/input.ts.hbs +16 -0
- package/templates/client/src/loading.css.hbs +19 -0
- package/templates/client/src/loading.ts.hbs +12 -0
- package/templates/client/src/title.css.hbs +16 -0
- package/templates/client/src/title.ts.hbs +16 -0
- package/templates/client/src/todoInput.css.hbs +9 -0
- package/templates/client/src/todoInput.ts.hbs +36 -0
- package/templates/client/src/todoItem.css.hbs +33 -0
- package/templates/client/src/todoItem.ts.hbs +31 -0
- package/templates/client/src/todoList.css.hbs +15 -0
- package/templates/client/src/todoList.ts.hbs +38 -0
- package/templates/client/src/topBar.css.hbs +13 -0
- package/templates/client/src/topBar.ts.hbs +13 -0
- package/templates/client/src/main.ts.hbs +0 -159
- package/templates/client/src/style.css.hbs +0 -199
package/README.md
CHANGED
|
@@ -1,20 +1,34 @@
|
|
|
1
1
|
# create-tinyjoin
|
|
2
2
|
|
|
3
|
-
Scaffold a small local todo app with TinyJoin
|
|
3
|
+
Scaffold a small local todo app with TinyJoin and Vite:
|
|
4
4
|
|
|
5
5
|
```sh
|
|
6
6
|
npm create tinyjoin@latest
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
The generated project is ready to run at its root and needs no database server,
|
|
10
|
-
account, or credentials. Choose
|
|
11
|
-
start fresh each time.
|
|
10
|
+
account, or credentials. Choose TypeScript or JavaScript, and whether its todos
|
|
11
|
+
should remain after reloads or start fresh each time.
|
|
12
|
+
|
|
13
|
+
Your app should look something like this:
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
- Task list with add/complete/delete
|
|
18
|
+
- Single TinyJoin database with one `todos` table
|
|
19
|
+
- Demonstrates basic CRUD operations in SQL
|
|
20
|
+
- Perfect starter example
|
|
21
|
+
|
|
22
|
+
It shares its markup and styling with the equivalent
|
|
23
|
+
[create-tinybase](https://github.com/tinyplex/create-tinybase) vanilla
|
|
24
|
+
TypeScript starter, apart from the TinyJoin accent color.
|
|
12
25
|
|
|
13
26
|
For agents and CI, every option can be supplied non-interactively:
|
|
14
27
|
|
|
15
28
|
```sh
|
|
16
29
|
npm create tinyjoin@latest -- --non-interactive \
|
|
17
30
|
--projectName my-tinyjoin-app \
|
|
31
|
+
--language typescript \
|
|
18
32
|
--storage opfs \
|
|
19
33
|
--installAndRun false
|
|
20
34
|
```
|
|
@@ -25,15 +39,49 @@ usage.
|
|
|
25
39
|
`npm run build` assembles the publishable `create-tinyjoin` package under
|
|
26
40
|
`dist/`.
|
|
27
41
|
|
|
42
|
+
## Generated structure
|
|
43
|
+
|
|
44
|
+
The generated project is a single Vite application at its root (`.ts` below
|
|
45
|
+
becomes `.js` when JavaScript is chosen):
|
|
46
|
+
|
|
47
|
+
- `index.html` loads the app and defines its theme variables.
|
|
48
|
+
- `src/index.ts` bootstraps the app on load.
|
|
49
|
+
- `src/app.ts` builds the app shell, showing a spinner until the database opens.
|
|
50
|
+
- `src/database.ts` opens the database, defines the `todos` table, and adds the
|
|
51
|
+
starter todos on the first visit.
|
|
52
|
+
- `src/todoInput.ts`, `src/todoList.ts`, and `src/todoItem.ts` are the todo
|
|
53
|
+
interface, each with a colocated stylesheet.
|
|
54
|
+
- `src/topBar.ts`, `src/title.ts`, `src/info.ts`, `src/loading.ts`,
|
|
55
|
+
`src/button.ts`, and `src/input.ts` are the shared interface pieces.
|
|
56
|
+
|
|
57
|
+
## TypeScript and JavaScript
|
|
58
|
+
|
|
59
|
+
The templates are written once, in TypeScript. Choosing JavaScript strips their
|
|
60
|
+
types with `ts-blank-space` and renames the outputs to `.js`, exactly as
|
|
61
|
+
`create-tinybase` does: `getFiles` marks each `.ts` template `transpile` when
|
|
62
|
+
the JavaScript language is chosen, and `tinycreate` blanks the types during
|
|
63
|
+
post-processing. A JavaScript project also drops `tsconfig.json`, the
|
|
64
|
+
`typescript` devDependency, and the `tsc --noEmit` step from its build script.
|
|
65
|
+
|
|
28
66
|
## Development
|
|
29
67
|
|
|
30
68
|
```sh
|
|
69
|
+
npm run spell # cspell over the sources, templates, and docs
|
|
31
70
|
npm run typecheck # generator and test TypeScript
|
|
32
|
-
npm test # CLI, template, and package-boundary tests
|
|
71
|
+
npm test # CLI, template, snapshot, and package-boundary tests
|
|
33
72
|
npm run test:generated # generated apps against published TinyJoin
|
|
34
73
|
npm run test:e2e # real generated-app behavior in Chromium
|
|
35
74
|
```
|
|
36
75
|
|
|
76
|
+
`npm run publishPackage` runs all of the above before publishing. Add project
|
|
77
|
+
vocabulary to `cspell.json` rather than disabling the check.
|
|
78
|
+
|
|
79
|
+
`npm test` compares every generated file, for both languages and both storage
|
|
80
|
+
modes, against the snapshots in `test/__snapshots__`. Run
|
|
81
|
+
`npx vitest run test/cli.test.ts -u` to accept intentional template changes.
|
|
82
|
+
`npm run test:generated` installs and builds all four combinations, and
|
|
83
|
+
`npm run test:e2e` then drives the two saved apps in Chromium.
|
|
84
|
+
|
|
37
85
|
Run the local generator from this repository's parent directory with:
|
|
38
86
|
|
|
39
87
|
```sh
|
package/cli.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import{existsSync as
|
|
2
|
+
import{existsSync as g}from"node:fs";import{dirname as m,join as r}from"node:path";import{fileURLToPath as d}from"node:url";import{createCLI as f,detectPackageManager as y}from"tinycreate";const h=r(m(d(import.meta.url)),"templates"),a=process.argv.slice(2),c=[{title:"TypeScript",value:"typescript"},{title:"JavaScript",value:"javascript"}],p=[{title:"Save data across reloads (recommended)",value:"opfs"},{title:"Start fresh each time",value:"memory"}],v={command:"npm create tinyjoin@latest --",nonInteractiveFlag:"--non-interactive",options:{projectName:{type:"string",required:!0},language:{values:c.map(({value:t})=>t),required:!0,default:"typescript"},storage:{values:p.map(({value:t})=>t),required:!0,default:"opfs"},installAndRun:{values:[!0,!1],required:!0,recommendedForAgents:!1}}};(a.includes("--help")||a.includes("-h"))&&(console.log(`create-tinyjoin
|
|
3
3
|
|
|
4
4
|
Interactively scaffold a TinyJoin application:
|
|
5
5
|
npm create tinyjoin@latest
|
|
6
6
|
|
|
7
7
|
Run non-interactively:
|
|
8
8
|
npm create tinyjoin@latest -- --non-interactive \\
|
|
9
|
-
--projectName my-tinyjoin-app --storage opfs \\
|
|
9
|
+
--projectName my-tinyjoin-app --language typescript --storage opfs \\
|
|
10
10
|
--installAndRun false
|
|
11
11
|
|
|
12
12
|
Agent and automation commands:
|
|
13
13
|
--list-options Print the current option catalog as JSON
|
|
14
|
-
--help Show this help`),process.exit(0)),
|
|
15
|
-
`,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinyjoin-app",validate:
|
|
14
|
+
--help Show this help`),process.exit(0)),a.includes("--list-options")&&(console.log(JSON.stringify(v,null,2)),process.exit(0));const j={welcomeMessage:`\u{1F389} Welcome to TinyJoin!
|
|
15
|
+
`,questions:[{type:"text",name:"projectName",message:"Project name:",initial:"my-tinyjoin-app",validate:l},{type:"select",name:"language",message:"Language:",choices:[...c],initial:0},{type:"select",name:"storage",message:"Todo data:",choices:[...p],initial:0},{type:"confirm",name:"installAndRun",message:"Install dependencies and start the app?",initial:!0}],createContext:t=>{const n=String(t.projectName??"").trim(),o=l(n);if(o!==!0)throw new TypeError(o);const e=u(t.language===0?"typescript":t.language??"typescript","language",["typescript","javascript"]),s=u(t.storage===0?"opfs":t.storage??"opfs","storage",["opfs","memory"]),i=e==="typescript";return{projectName:n,installAndRun:t.installAndRun===!0||t.installAndRun==="true",language:e,typescript:i,javascript:!i,scriptExt:i?"ts":"js",ext:i?"ts":"js",storage:s,usesOpfs:s==="opfs",storageName:b(n),tinyjoinDependency:process.env.CREATE_TINYJOIN_DEPENDENCY??"^0.0.5"}},getFiles:t=>{const{ext:n,typescript:o}=t,e=i=>({template:`client/src/${i}.ts.hbs`,output:`src/${i}.${n}`,prettier:!0,transpile:!o}),s=i=>({template:`client/src/${i}.css.hbs`,output:`src/${i}.css`,prettier:!0});return[{template:"README.md.hbs",output:"README.md",prettier:!0},{template:"AGENTS.md.hbs",output:"AGENTS.md",prettier:!0},{template:"client/package.json.hbs",output:"package.json",prettier:!0},{template:"client/.gitignore.hbs",output:".gitignore"},{template:"client/index.html.hbs",output:"index.html",prettier:!0},...o?[{template:"client/tsconfig.json.hbs",output:"tsconfig.json",prettier:!0}]:[],{template:"client/public/favicon.svg",output:"public/favicon.svg"},o?{template:"client/public/ts.svg",output:"public/ts.svg"}:{template:"client/public/js.svg",output:"public/js.svg"},e("index"),e("app"),e("topBar"),s("topBar"),e("title"),s("title"),e("info"),s("info"),e("loading"),s("loading"),e("button"),s("button"),e("input"),s("input"),e("database"),e("todoInput"),s("todoInput"),e("todoList"),s("todoList"),e("todoItem"),s("todoItem")]},templateRoot:h,installCommand:"{pm} install",devCommand:"{pm} run dev",workingDirectory:".",onSuccess:t=>{const n=y();console.log("Next steps:"),console.log(` cd ${t}`),console.log(` ${n} install`),console.log(` ${n} run dev`)}};f(j).catch(t=>{console.error(t instanceof Error?t.message:String(t)),process.exit(1)});function l(t){return!/^[a-z0-9][a-z0-9._-]*$/i.test(t)||t==="."||t===".."?"Use letters, numbers, dots, dashes, or underscores without path separators.":g(r(process.cwd(),t))?`Directory "${t}" already exists. Choose a different name.`:!0}function u(t,n,o){const e=typeof t=="string"?t:"";if(!o.includes(e))throw new TypeError(`${x(n)} must be one of: ${o.join(", ")}.`);return e}function b(t){const n=t.toLowerCase().replace(/[^a-z0-9]+/g,"-").replace(/^-+|-+$/g,"")||"app",o="tinyjoin-",e="-db-v1";return`${o}${n.slice(0,64-o.length-e.length)}${e}`}function x(t){return`${t.charAt(0).toUpperCase()}${t.slice(1)}`}
|
package/package.json
CHANGED
package/templates/AGENTS.md.hbs
CHANGED
|
@@ -1,15 +1,52 @@
|
|
|
1
|
-
#
|
|
1
|
+
# TinyJoin Application Guide
|
|
2
2
|
|
|
3
|
-
This
|
|
3
|
+
This project was generated by `create-tinyjoin`. Preserve its working data
|
|
4
|
+
lifecycle while adapting the application to the user's requirements.
|
|
4
5
|
|
|
5
|
-
|
|
6
|
-
- Keep database setup in `src/database.ts` and application UI in `src/main.ts`.
|
|
7
|
-
- Keep schema setup safe to run again when the app reloads.
|
|
8
|
-
- Use TinyJoin's `sql` tagged template or parameter arrays for application
|
|
9
|
-
values. Do not concatenate user input into SQL.
|
|
10
|
-
- Generate stable row identifiers in application code.
|
|
11
|
-
- Keep the database client open while the app is running and close it during
|
|
12
|
-
page teardown.
|
|
13
|
-
- Validate changes with `npm run build`.
|
|
6
|
+
## Selected Architecture
|
|
14
7
|
|
|
15
|
-
|
|
8
|
+
- **Application**: todo list
|
|
9
|
+
- **Language**: {{#if typescript}}TypeScript{{else}}JavaScript{{/if}}
|
|
10
|
+
- **UI**: Vanilla JS
|
|
11
|
+
- **Data**: TinyJoin relational database in the browser
|
|
12
|
+
- **Persistence**: {{#if usesOpfs}}saved in this browser across reloads{{else}}kept for the current visit only{{/if}}
|
|
13
|
+
|
|
14
|
+
The database client is opened once in `src/database.{{ext}}` and shared through
|
|
15
|
+
the `databaseReady` promise. Keep that single client: opening a second one for
|
|
16
|
+
the same data is rejected.
|
|
17
|
+
|
|
18
|
+
The schema is created before the first read, and the starter rows are only
|
|
19
|
+
inserted when the table is empty. Preserve that ordering so a reload does not
|
|
20
|
+
duplicate or overwrite existing data.
|
|
21
|
+
|
|
22
|
+
## Key Files
|
|
23
|
+
|
|
24
|
+
- `README.md` explains how to run the generated project.
|
|
25
|
+
- `src/database.{{ext}}` opens the database and defines the `todos` table.
|
|
26
|
+
- `src/app.{{ext}}` contains the main application UI.
|
|
27
|
+
- `src/todoList.{{ext}}` reads the `todos` table and subscribes to its changes.
|
|
28
|
+
|
|
29
|
+
## Working Method
|
|
30
|
+
|
|
31
|
+
1. Read `README.md` and `src/database.{{ext}}` before making changes.
|
|
32
|
+
2. Keep application data in TinyJoin rather than duplicating it in UI state.
|
|
33
|
+
3. Use the `sql` tagged template or `$1` parameter arrays for application
|
|
34
|
+
values. Do not concatenate user input into SQL.
|
|
35
|
+
4. Generate stable row identifiers in application code.
|
|
36
|
+
5. Keep the schema safe to run again when the app reloads.
|
|
37
|
+
6. Keep the database client open while the app is running and close it during
|
|
38
|
+
page teardown.
|
|
39
|
+
7. Run the build after edits.
|
|
40
|
+
|
|
41
|
+
## Verification
|
|
42
|
+
|
|
43
|
+
Run:
|
|
44
|
+
|
|
45
|
+
```sh
|
|
46
|
+
npm run build
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Change data in the running app, reload the page, and confirm the change
|
|
50
|
+
{{#if usesOpfs}}remains{{else}}resets, as this starter was generated without persistence{{/if}}.
|
|
51
|
+
|
|
52
|
+
Current TinyJoin documentation: https://tinyjoin.org/llms.txt
|
package/templates/README.md.hbs
CHANGED
|
@@ -1,29 +1,54 @@
|
|
|
1
1
|
# {{projectName}}
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A TinyJoin todo app built with {{#if typescript}}TypeScript{{else}}JavaScript{{/if}} and Vanilla JS.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Getting Started
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
To start the web client with Vite, run the following commands:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
8
10
|
npm install
|
|
9
11
|
npm run dev
|
|
10
12
|
```
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
PNPM, Yarn, and Bun should also work. Your app will be available at
|
|
15
|
+
`http://localhost:5173`, or whichever port Vite specifies in the console. It
|
|
16
|
+
needs no database server, account, or credentials.
|
|
17
|
+
|
|
18
|
+
After that, start hacking!
|
|
19
|
+
|
|
20
|
+
## Structure
|
|
21
|
+
|
|
22
|
+
This project is a single web application, built with Vite and
|
|
23
|
+
{{#if typescript}}TypeScript{{else}}JavaScript{{/if}}.
|
|
24
|
+
|
|
25
|
+
### Key Files
|
|
26
|
+
|
|
27
|
+
- **`index.html`**: The main HTML file that loads your app
|
|
28
|
+
- **`src/index.{{ext}}`**: Entry point that bootstraps the app
|
|
29
|
+
- **`src/app.{{ext}}`**: Main application logic
|
|
30
|
+
- **`src/database.{{ext}}`**: TinyJoin database and `todos` table setup
|
|
31
|
+
- **`src/todoInput.{{ext}}`**, **`src/todoList.{{ext}}`**,
|
|
32
|
+
**`src/todoItem.{{ext}}`**: The todo interface
|
|
33
|
+
- **`src/topBar.{{ext}}`**, **`src/button.{{ext}}`**, **`src/input.{{ext}}`**,
|
|
34
|
+
**`src/loading.{{ext}}`**: Shared interface pieces
|
|
35
|
+
|
|
36
|
+
#### How It Works
|
|
37
|
+
|
|
38
|
+
The app uses **TinyJoin** to keep application state in a small relational
|
|
39
|
+
database that runs entirely in the browser. The interface reads rows with
|
|
40
|
+
ordinary SQL queries, writes them with parameterized statements, and subscribes
|
|
41
|
+
to the `todos` table so that every change re-renders the list.
|
|
14
42
|
|
|
15
43
|
{{#if usesOpfs}}
|
|
16
|
-
|
|
44
|
+
**Persistence** is enabled. Your data is automatically saved and restored when
|
|
45
|
+
you reload the page.
|
|
17
46
|
{{else}}
|
|
18
|
-
|
|
47
|
+
**Persistence** is not enabled. Your todos are kept for the current visit and
|
|
48
|
+
start fresh after a reload.
|
|
19
49
|
{{/if}}
|
|
20
50
|
|
|
21
|
-
##
|
|
22
|
-
|
|
23
|
-
- `src/database.ts` opens the database and defines the `todos` table.
|
|
24
|
-
- `src/main.ts` contains the todo UI and its queries.
|
|
25
|
-
- `src/style.css` contains the app styles.
|
|
26
|
-
|
|
27
|
-
Run `npm run build` to check the TypeScript and create a production build.
|
|
51
|
+
## Learn More
|
|
28
52
|
|
|
29
|
-
|
|
53
|
+
- [TinyJoin Documentation](https://tinyjoin.org)
|
|
54
|
+
- [TinyJoin Demos](https://tinyjoin.org/demos/)
|
|
@@ -1,51 +1,57 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en">
|
|
3
|
-
<head>
|
|
4
|
-
<meta charset="UTF-8" />
|
|
5
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
-
<meta
|
|
7
|
-
name="description"
|
|
8
|
-
content="A small local todo app built with TinyJoin"
|
|
9
|
-
/>
|
|
10
|
-
<title>{{projectName}}</title>
|
|
11
|
-
</head>
|
|
12
|
-
<body>
|
|
13
|
-
<main>
|
|
14
|
-
<header>
|
|
15
|
-
<p class="eyebrow">TinyJoin starter</p>
|
|
16
|
-
<h1>Todos</h1>
|
|
17
|
-
<p class="intro">
|
|
18
|
-
{{#if usesOpfs}}Your list is saved locally in this browser.{{else}}Your list starts fresh each time you load the app.{{/if}}
|
|
19
|
-
</p>
|
|
20
|
-
</header>
|
|
21
3
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
/>
|
|
35
|
-
<button type="submit">Add</button>
|
|
36
|
-
</div>
|
|
37
|
-
</form>
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
|
+
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
8
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
9
|
+
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;800&display=swap" />
|
|
10
|
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
11
|
+
<title>TinyJoin Todos</title>
|
|
12
|
+
<style>
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
}
|
|
38
16
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
17
|
+
body {
|
|
18
|
+
user-select: none;
|
|
19
|
+
margin: 0;
|
|
20
|
+
padding: 0;
|
|
21
|
+
font-family: 'Inter', sans-serif;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#root {
|
|
25
|
+
display: flex;
|
|
26
|
+
flex-direction: column;
|
|
27
|
+
min-height: 100vh;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#app {
|
|
31
|
+
max-width: 60rem;
|
|
32
|
+
width: 100%;
|
|
33
|
+
margin: 0 auto;
|
|
34
|
+
padding: 2rem 1.5rem;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
body {
|
|
38
|
+
--hue: 270;
|
|
39
|
+
--fg: oklch(85% 0.01 var(--hue));
|
|
40
|
+
--fg2: oklch(60% 0.01 var(--hue));
|
|
41
|
+
--bg: oklch(20% 0.01 var(--hue));
|
|
42
|
+
--bg2: oklch(25% 0.01 var(--hue));
|
|
43
|
+
--border: oklch(30% 0.01 var(--hue));
|
|
44
|
+
--bg-header: oklch(30% 0.008 var(--hue) / 0.5);
|
|
45
|
+
--accent: #7c3aed;
|
|
46
|
+
color: var(--fg);
|
|
47
|
+
background: var(--bg);
|
|
48
|
+
}
|
|
49
|
+
</style>
|
|
50
|
+
</head>
|
|
51
|
+
|
|
52
|
+
<body>
|
|
53
|
+
<div id="root"></div>
|
|
54
|
+
<script type="module" src="/src/index.{{ext}}"></script>
|
|
55
|
+
</body>
|
|
44
56
|
|
|
45
|
-
<footer>
|
|
46
|
-
Built with <a href="https://tinyjoin.org">TinyJoin</a>.
|
|
47
|
-
</footer>
|
|
48
|
-
</main>
|
|
49
|
-
<script type="module" src="/src/main.ts"></script>
|
|
50
|
-
</body>
|
|
51
57
|
</html>
|
|
@@ -8,14 +8,16 @@
|
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
10
|
"dev": "vite",
|
|
11
|
-
"build": "tsc --noEmit && vite build",
|
|
11
|
+
"build": "{{#if typescript}}tsc --noEmit && {{/if}}vite build",
|
|
12
12
|
"preview": "vite preview"
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"tinyjoin": "{{tinyjoinDependency}}"
|
|
16
16
|
},
|
|
17
17
|
"devDependencies": {
|
|
18
|
+
{{#if typescript}}
|
|
18
19
|
"typescript": "^7.0.2",
|
|
20
|
+
{{/if}}
|
|
19
21
|
"vite": "^8.2.1"
|
|
20
22
|
}
|
|
21
23
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="-100 -100 1200 1200" stroke-linejoin="round" stroke="#fff" stroke-width="50">
|
|
2
|
+
<path d="M0 150a500 150 0 00223 125 100 100 0 0179 84l48 291v250a100 100 0 00100 100h100a100 100 0 00100-100V650l48-291a100 100 0 0179-84 500 150 0 00223-125A500 150 0 000 150z" fill="#7C3AED" />
|
|
3
|
+
<path
|
|
4
|
+
d="M0 150a500 150 0 00223 125 100 100 0 0179 84 700 700 0 01-12.5 160A500 150 0 010 383zm0 233a500 150 0 00289.5 136A700 700 0 01206 738.5 500 150 0 010 617zm0 234a500 150 0 00206 121.5 700 700 0 01-81 81.5 500 500 0 00225-90v170a100 100 0 00100 100A500 150 0 010 850zm1000-467a500 150 0 01-223 125 100 100 0 00-79 84 700 700 0 0012.5 160A500 150 0 001000 383zm0 233a500 150 0 01-289.5 136A700 700 0 00794 738.5 500 150 0 001000 617zm0 234a500 150 0 01-206 121.5 700 700 0 0081 81.5 500 500 0 01-225-90v170a100 100 0 01-100 100 500 150 0 00450-150z" />
|
|
5
|
+
<path d="M350 730a500 500 0 01-225 90 700 700 0 00177-461l48 291zm300 0a500 500 0 00225 90 700 700 0 01-177-461l-48 291z" fill="#fff" />
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 630 630">
|
|
2
|
+
<rect width="630" height="630" fill="#f7df1e" />
|
|
3
|
+
<path
|
|
4
|
+
d="m423.2 492.19c12.69 20.72 29.2 35.95 58.4 35.95 24.53 0 40.2-12.26 40.2-29.2 0-20.3-16.1-27.49-43.1-39.3l-14.8-6.35c-42.72-18.2-71.1-41-71.1-89.2 0-44.4 33.83-78.2 86.7-78.2 37.64 0 64.7 13.1 84.2 47.4l-46.1 29.6c-10.15-18.2-21.1-25.37-38.1-25.37-17.34 0-28.33 11-28.33 25.37 0 17.76 11 24.95 36.4 35.95l14.8 6.34c50.3 21.57 78.7 43.56 78.7 93 0 53.3-41.87 82.5-98.1 82.5-54.98 0-90.5-26.2-107.88-60.54zm-209.13 5.13c9.3 16.5 17.76 30.45 38.1 30.45 19.45 0 31.72-7.61 31.72-37.2v-201.3h59.2v202.1c0 61.3-35.94 89.2-88.4 89.2-47.4 0-74.85-24.53-88.81-54.075z" />
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg fill="none" height="512" viewBox="0 0 512 512" width="512" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<rect fill="#3178c6" height="512" width="512" />
|
|
3
|
+
<path clip-rule="evenodd"
|
|
4
|
+
d="m316.939 407.424v50.061c8.138 4.172 17.763 7.3 28.875 9.386s22.823 3.129 35.135 3.129c11.999 0 23.397-1.147 34.196-3.442 10.799-2.294 20.268-6.075 28.406-11.342 8.138-5.266 14.581-12.15 19.328-20.65s7.121-19.007 7.121-31.522c0-9.074-1.356-17.026-4.069-23.857s-6.625-12.906-11.738-18.225c-5.112-5.319-11.242-10.091-18.389-14.315s-15.207-8.213-24.18-11.967c-6.573-2.712-12.468-5.345-17.685-7.9-5.217-2.556-9.651-5.163-13.303-7.822-3.652-2.66-6.469-5.476-8.451-8.448-1.982-2.973-2.974-6.336-2.974-10.091 0-3.441.887-6.544 2.661-9.308s4.278-5.136 7.512-7.118c3.235-1.981 7.199-3.52 11.894-4.615 4.696-1.095 9.912-1.642 15.651-1.642 4.173 0 8.581.313 13.224.938 4.643.626 9.312 1.591 14.008 2.894 4.695 1.304 9.259 2.947 13.694 4.928 4.434 1.982 8.529 4.276 12.285 6.884v-46.776c-7.616-2.92-15.937-5.084-24.962-6.492s-19.381-2.112-31.066-2.112c-11.895 0-23.163 1.278-33.805 3.833s-20.006 6.544-28.093 11.967c-8.086 5.424-14.476 12.333-19.171 20.729-4.695 8.395-7.043 18.433-7.043 30.114 0 14.914 4.304 27.638 12.912 38.172 8.607 10.533 21.675 19.45 39.204 26.751 6.886 2.816 13.303 5.579 19.25 8.291s11.086 5.528 15.415 8.448c4.33 2.92 7.747 6.101 10.252 9.543 2.504 3.441 3.756 7.352 3.756 11.733 0 3.233-.783 6.231-2.348 8.995s-3.939 5.162-7.121 7.196-7.147 3.624-11.894 4.771c-4.748 1.148-10.303 1.721-16.668 1.721-10.851 0-21.597-1.903-32.24-5.71-10.642-3.806-20.502-9.516-29.579-17.13zm-84.159-123.342h64.22v-41.082h-179v41.082h63.906v182.918h50.874z"
|
|
5
|
+
fill="#fff" fill-rule="evenodd" />
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {databaseReady} from './database';
|
|
2
|
+
import {createTopBar} from './topBar';
|
|
3
|
+
import {showLoading, hideLoading} from './loading';
|
|
4
|
+
import {createTodoInput} from './todoInput';
|
|
5
|
+
import {createTodoList} from './todoList';
|
|
6
|
+
|
|
7
|
+
export const app = async (root: HTMLElement) => {
|
|
8
|
+
const appElement = document.createElement('div');
|
|
9
|
+
appElement.id = 'app';
|
|
10
|
+
root.replaceChildren(createTopBar(), appElement);
|
|
11
|
+
|
|
12
|
+
// Show loading spinner
|
|
13
|
+
const loadingDiv = showLoading(appElement);
|
|
14
|
+
|
|
15
|
+
// Wait for the database to be ready
|
|
16
|
+
const database = await databaseReady;
|
|
17
|
+
|
|
18
|
+
// Remove loading spinner
|
|
19
|
+
hideLoading(loadingDiv);
|
|
20
|
+
|
|
21
|
+
const todoInputContainer = createTodoInput(database);
|
|
22
|
+
appElement.appendChild(todoInputContainer);
|
|
23
|
+
|
|
24
|
+
const input = todoInputContainer.querySelector('input')!;
|
|
25
|
+
input.focus();
|
|
26
|
+
|
|
27
|
+
appElement.appendChild(createTodoList(database));
|
|
28
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
button {
|
|
2
|
+
padding: 0.5rem 1rem;
|
|
3
|
+
border: 1px solid #666;
|
|
4
|
+
border-radius: 0.25rem;
|
|
5
|
+
background: #222;
|
|
6
|
+
color: #fff;
|
|
7
|
+
font-family: inherit;
|
|
8
|
+
font-weight: 800;
|
|
9
|
+
cursor: pointer;
|
|
10
|
+
margin: 0.5rem;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
button:hover {
|
|
14
|
+
border-color: #7c3aed;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
button.primary {
|
|
18
|
+
background: #7c3aed;
|
|
19
|
+
border: none;
|
|
20
|
+
padding: 0.75rem 1.5rem;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
button.primary:hover {
|
|
24
|
+
background: #6d28d9;
|
|
25
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import './button.css';
|
|
2
|
+
|
|
3
|
+
export const createButton = (
|
|
4
|
+
text: string,
|
|
5
|
+
onClick: (() => void) | null,
|
|
6
|
+
variant: 'default' | 'primary' = 'default',
|
|
7
|
+
type: 'button' | 'submit' = 'button',
|
|
8
|
+
): HTMLButtonElement => {
|
|
9
|
+
const button = document.createElement('button');
|
|
10
|
+
button.textContent = text;
|
|
11
|
+
button.type = type;
|
|
12
|
+
if (variant === 'primary') {
|
|
13
|
+
button.className = 'primary';
|
|
14
|
+
}
|
|
15
|
+
if (onClick) {
|
|
16
|
+
button.addEventListener('click', onClick);
|
|
17
|
+
}
|
|
18
|
+
return button;
|
|
19
|
+
};
|
|
@@ -1,29 +1,56 @@
|
|
|
1
|
-
import {create} from 'tinyjoin';
|
|
1
|
+
import {create, type Client} from 'tinyjoin';
|
|
2
2
|
|
|
3
|
-
export type
|
|
3
|
+
export type TodoRow = {
|
|
4
4
|
id: string;
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
text: string;
|
|
6
|
+
completed: boolean;
|
|
7
|
+
created: number;
|
|
7
8
|
};
|
|
8
9
|
|
|
9
|
-
export type
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
10
|
+
export type TodosDatabase = Client;
|
|
11
|
+
|
|
12
|
+
{{#if usesOpfs}}
|
|
13
|
+
export const DATA_DIR = 'opfs://{{storageName}}';
|
|
14
|
+
{{else}}
|
|
15
|
+
export const DATA_DIR = 'memory://';
|
|
16
|
+
{{/if}}
|
|
17
|
+
|
|
18
|
+
export const createTodosDatabase = async (): Promise<TodosDatabase> => {
|
|
19
|
+
const database = await create(DATA_DIR);
|
|
20
|
+
|
|
21
|
+
// Create the table, and add the starter todos, only on the first visit.
|
|
22
|
+
if (!(await hasTodosTable(database))) {
|
|
23
|
+
await database.exec(`
|
|
24
|
+
CREATE TABLE IF NOT EXISTS todos (
|
|
25
|
+
id TEXT PRIMARY KEY,
|
|
26
|
+
text TEXT NOT NULL,
|
|
27
|
+
completed BOOLEAN NOT NULL DEFAULT false,
|
|
28
|
+
created INTEGER NOT NULL DEFAULT 0
|
|
29
|
+
)
|
|
30
|
+
`);
|
|
31
|
+
await database.query(
|
|
32
|
+
`INSERT INTO todos (id, text, completed, created)
|
|
33
|
+
VALUES ($1, $2, $3, $4), ($5, $6, $7, $8)`,
|
|
34
|
+
['1', 'Learn TinyJoin', false, 1, '2', 'Build an app', false, 2],
|
|
35
|
+
);
|
|
36
|
+
}
|
|
27
37
|
|
|
28
38
|
return database;
|
|
29
|
-
}
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const hasTodosTable = async (database: TodosDatabase): Promise<boolean> => {
|
|
42
|
+
try {
|
|
43
|
+
await database.query('SELECT id FROM todos LIMIT 1');
|
|
44
|
+
return true;
|
|
45
|
+
} catch {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export const databaseReady = createTodosDatabase();
|
|
51
|
+
|
|
52
|
+
addEventListener(
|
|
53
|
+
'pagehide',
|
|
54
|
+
() => void databaseReady.then((database) => database.close()),
|
|
55
|
+
{once: true},
|
|
56
|
+
);
|