create-elit 3.6.5 → 3.6.7
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 +32 -68
- package/dist/index.js +121 -8
- package/dist/templates/{elit.config.ts → auth-fullstack-example/elit.config.ts} +51 -0
- package/dist/templates/auth-fullstack-example/package.json +26 -0
- package/dist/templates/auth-fullstack-example/src/native-screen.ts +10 -0
- package/dist/templates/auth-fullstack-example/wapkignore +10 -0
- package/dist/templates/auth-fullstack-example/wapkpatch +1 -0
- package/dist/templates/basic-example/README.md +39 -0
- package/dist/templates/basic-example/elit.config.ts +114 -0
- package/dist/templates/basic-example/gitignore +7 -0
- package/dist/templates/basic-example/package.json +26 -0
- package/dist/templates/basic-example/public/favicon.svg +22 -0
- package/dist/templates/basic-example/public/index.html +14 -0
- package/dist/templates/basic-example/src/client.ts +15 -0
- package/dist/templates/basic-example/src/main.ts +89 -0
- package/dist/templates/basic-example/src/mobile.ts +35 -0
- package/dist/templates/basic-example/src/styles.ts +273 -0
- package/dist/templates/basic-example/tsconfig.json +24 -0
- package/dist/templates/basic-example/wapkignore +10 -0
- package/dist/templates/basic-example/wapkpatch +1 -0
- package/dist/templates/todo-fullstack-example/README.md +39 -0
- package/dist/templates/todo-fullstack-example/databases/todo.ts +41 -0
- package/dist/templates/todo-fullstack-example/elit.config.ts +123 -0
- package/dist/templates/todo-fullstack-example/gitignore +7 -0
- package/dist/templates/todo-fullstack-example/package.json +26 -0
- package/dist/templates/todo-fullstack-example/public/favicon.svg +22 -0
- package/dist/templates/todo-fullstack-example/public/index.html +14 -0
- package/dist/templates/todo-fullstack-example/src/client.ts +15 -0
- package/dist/templates/todo-fullstack-example/src/components/AppFooter.ts +16 -0
- package/dist/templates/todo-fullstack-example/src/components/AppHeader.ts +23 -0
- package/dist/templates/todo-fullstack-example/src/main.ts +7 -0
- package/dist/templates/todo-fullstack-example/src/mobile.ts +36 -0
- package/dist/templates/todo-fullstack-example/src/pages/TodoPage.ts +491 -0
- package/dist/templates/todo-fullstack-example/src/router.ts +16 -0
- package/dist/templates/todo-fullstack-example/src/server.ts +226 -0
- package/dist/templates/todo-fullstack-example/src/styles.ts +768 -0
- package/dist/templates/todo-fullstack-example/src/todo-types.ts +19 -0
- package/dist/templates/todo-fullstack-example/src/web.ts +16 -0
- package/dist/templates/todo-fullstack-example/tsconfig.json +24 -0
- package/dist/templates/todo-fullstack-example/wapkignore +10 -0
- package/dist/templates/todo-fullstack-example/wapkpatch +1 -0
- package/package.json +1 -1
- package/dist/templates/package.json +0 -17
- package/dist/templates/src/client.test.ts +0 -292
- package/dist/templates/src/components/Footer.test.ts +0 -226
- package/dist/templates/src/components/Header.test.ts +0 -493
- package/dist/templates/src/pages/ChatListPage.test.ts +0 -603
- package/dist/templates/src/pages/ChatPage.test.ts +0 -530
- package/dist/templates/src/pages/ForgotPasswordPage.test.ts +0 -484
- package/dist/templates/src/pages/HomePage.test.ts +0 -601
- package/dist/templates/src/pages/LoginPage.test.ts +0 -619
- package/dist/templates/src/pages/PrivateChatPage.test.ts +0 -556
- package/dist/templates/src/pages/ProfilePage.test.ts +0 -628
- package/dist/templates/src/pages/RegisterPage.test.ts +0 -661
- /package/dist/templates/{README.md → auth-fullstack-example/README.md} +0 -0
- /package/dist/templates/{databases → auth-fullstack-example/databases}/users.ts +0 -0
- /package/dist/templates/{gitignore → auth-fullstack-example/gitignore} +0 -0
- /package/dist/templates/{public → auth-fullstack-example/public}/favicon.svg +0 -0
- /package/dist/templates/{public → auth-fullstack-example/public}/index.html +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/client.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/components/Footer.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/components/Header.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/components/index.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/main.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/ChatListPage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/ChatPage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/ForgotPasswordPage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/HomePage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/LoginPage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/PrivateChatPage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/ProfilePage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/pages/RegisterPage.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/router.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/server.ts +0 -0
- /package/dist/templates/{src → auth-fullstack-example/src}/styles.ts +0 -0
- /package/dist/templates/{tsconfig.json → auth-fullstack-example/tsconfig.json} +0 -0
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="grad" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
4
|
+
<stop offset="0%" stop-color="#6366f1"/>
|
|
5
|
+
<stop offset="100%" stop-color="#8b5cf6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
</defs>
|
|
8
|
+
|
|
9
|
+
<!-- Clean background -->
|
|
10
|
+
<rect width="100" height="100" rx="20" fill="url(#grad)"/>
|
|
11
|
+
|
|
12
|
+
<!-- Simple E shape - 3 horizontal bars -->
|
|
13
|
+
<rect x="28" y="25" width="44" height="8" rx="4" fill="white"/>
|
|
14
|
+
<rect x="28" y="46" width="32" height="8" rx="4" fill="white"/>
|
|
15
|
+
<rect x="28" y="67" width="44" height="8" rx="4" fill="white"/>
|
|
16
|
+
|
|
17
|
+
<!-- Vertical connector -->
|
|
18
|
+
<rect x="28" y="25" width="8" height="50" rx="4" fill="white"/>
|
|
19
|
+
|
|
20
|
+
<!-- Single accent dot -->
|
|
21
|
+
<circle cx="72" cy="50" r="6" fill="white" opacity="0.5"/>
|
|
22
|
+
</svg>
|
|
@@ -0,0 +1,14 @@
|
|
|
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.0">
|
|
6
|
+
<title>ELIT_PROJECT_NAME - Todo Workspace</title>
|
|
7
|
+
<link rel="icon" type="image/svg+xml" href="favicon.svg">
|
|
8
|
+
<meta name="description" content="Database-backed todo starter built with Elit and elit/database.">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="app"></div>
|
|
12
|
+
<script type="module" src="../src/main.ts"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { div, html, head, body, title, link, script, meta } from 'elit/el';
|
|
2
|
+
|
|
3
|
+
export const client = html(
|
|
4
|
+
head(
|
|
5
|
+
title('ELIT_PROJECT_NAME - Todo Workspace'),
|
|
6
|
+
link({ rel: 'icon', type: 'image/svg+xml', href: 'public/favicon.svg' }),
|
|
7
|
+
meta({ charset: 'UTF-8' }),
|
|
8
|
+
meta({ name: 'viewport', content: 'width=device-width, initial-scale=1.0' }),
|
|
9
|
+
meta({ name: 'description', content: 'Database-backed todo starter built with Elit and elit/database.' })
|
|
10
|
+
),
|
|
11
|
+
body(
|
|
12
|
+
div({ id: 'app' }),
|
|
13
|
+
script({ type: 'module', src: '/src/main.js' })
|
|
14
|
+
)
|
|
15
|
+
);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { a, div, footer, p } from 'elit/el';
|
|
2
|
+
|
|
3
|
+
export function AppFooter() {
|
|
4
|
+
return footer({ className: 'app-footer' },
|
|
5
|
+
div({ className: 'app-footer-inner' },
|
|
6
|
+
p({ className: 'footer-copy' },
|
|
7
|
+
'Built with Elit. Persisted with elit/database. Ready for your own workflows, rules, and team-specific polish.'
|
|
8
|
+
),
|
|
9
|
+
div({ className: 'footer-links' },
|
|
10
|
+
a({ href: 'https://d-osc.github.io/elit/#/docs', target: '_blank', className: 'footer-link' }, 'Documentation'),
|
|
11
|
+
a({ href: 'https://github.com/d-osc/elit', target: '_blank', className: 'footer-link' }, 'GitHub'),
|
|
12
|
+
a({ href: '#', className: 'footer-link' }, 'databases/todo.ts')
|
|
13
|
+
)
|
|
14
|
+
)
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { div, h1, header, p, span } from 'elit/el';
|
|
2
|
+
import { routerLink } from 'elit/router';
|
|
3
|
+
import type { Router } from 'elit';
|
|
4
|
+
|
|
5
|
+
export function AppHeader(router: Router) {
|
|
6
|
+
return header({ className: 'app-header' },
|
|
7
|
+
div({ className: 'app-header-inner' },
|
|
8
|
+
div({ className: 'brand-block' },
|
|
9
|
+
routerLink(router, { to: '/', className: 'brand-link' },
|
|
10
|
+
span({ className: 'brand-mark' }, 'EL'),
|
|
11
|
+
div({ className: 'brand-title-group' },
|
|
12
|
+
h1({ className: 'brand-title' }, 'ELIT_PROJECT_NAME'),
|
|
13
|
+
p({ className: 'brand-subtitle' }, 'A fullstack todo starter that writes directly to databases/todo.ts.')
|
|
14
|
+
)
|
|
15
|
+
)
|
|
16
|
+
),
|
|
17
|
+
div({ className: 'header-pill' },
|
|
18
|
+
span({ className: 'header-pill-label' }, 'Storage'),
|
|
19
|
+
span({ className: 'header-pill-value' }, 'elit/database')
|
|
20
|
+
)
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { button, div, h1, input, p, span } from 'elit/el';
|
|
2
|
+
|
|
3
|
+
function mobileTodo(label: string, checked: boolean) {
|
|
4
|
+
return div(
|
|
5
|
+
{
|
|
6
|
+
style: {
|
|
7
|
+
display: 'flex',
|
|
8
|
+
gap: '12px',
|
|
9
|
+
alignItems: 'center',
|
|
10
|
+
padding: '12px 14px',
|
|
11
|
+
borderRadius: '14px',
|
|
12
|
+
background: '#ffffff'
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
input({ type: 'checkbox', checked }),
|
|
16
|
+
span(label)
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const screen = () => div(
|
|
21
|
+
{
|
|
22
|
+
style: {
|
|
23
|
+
padding: '24px',
|
|
24
|
+
display: 'flex',
|
|
25
|
+
flexDirection: 'column',
|
|
26
|
+
gap: '18px',
|
|
27
|
+
background: '#f7efe8'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
h1('Todo Companion'),
|
|
31
|
+
p('A native-friendly preview for the fullstack todo starter.'),
|
|
32
|
+
mobileTodo('Review the database-backed API routes', true),
|
|
33
|
+
mobileTodo('Add your first team-specific workflow', false),
|
|
34
|
+
mobileTodo('Clear the sample tasks before launch', false),
|
|
35
|
+
button({ onClick: () => undefined }, 'Sync mobile shell')
|
|
36
|
+
);
|
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
import {
|
|
2
|
+
article,
|
|
3
|
+
button,
|
|
4
|
+
div,
|
|
5
|
+
form,
|
|
6
|
+
h1,
|
|
7
|
+
h2,
|
|
8
|
+
h3,
|
|
9
|
+
input,
|
|
10
|
+
label,
|
|
11
|
+
option,
|
|
12
|
+
p,
|
|
13
|
+
section,
|
|
14
|
+
select,
|
|
15
|
+
span,
|
|
16
|
+
textarea
|
|
17
|
+
} from 'elit/el';
|
|
18
|
+
import { bindValue, createState, reactive } from 'elit/state';
|
|
19
|
+
import type { TodoFilter, TodoItem, TodoPriority, TodoSummary } from '../todo-types';
|
|
20
|
+
|
|
21
|
+
interface TodoResponse {
|
|
22
|
+
message?: string;
|
|
23
|
+
todos: TodoItem[];
|
|
24
|
+
summary: TodoSummary;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const emptySummary: TodoSummary = {
|
|
28
|
+
total: 0,
|
|
29
|
+
active: 0,
|
|
30
|
+
completed: 0,
|
|
31
|
+
highPriority: 0
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const priorityLabel: Record<TodoPriority, string> = {
|
|
35
|
+
high: 'Ship first',
|
|
36
|
+
medium: 'Steady pace',
|
|
37
|
+
low: 'Backlog'
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
const priorityWeight: Record<TodoPriority, number> = {
|
|
41
|
+
high: 0,
|
|
42
|
+
medium: 1,
|
|
43
|
+
low: 2
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function summarizeTodos(items: TodoItem[]): TodoSummary {
|
|
47
|
+
const completed = items.filter((todo) => todo.completed).length;
|
|
48
|
+
const active = items.length - completed;
|
|
49
|
+
const highPriority = items.filter((todo) => !todo.completed && todo.priority === 'high').length;
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
total: items.length,
|
|
53
|
+
active,
|
|
54
|
+
completed,
|
|
55
|
+
highPriority
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function formatDate(value: string): string {
|
|
60
|
+
return new Intl.DateTimeFormat('en', {
|
|
61
|
+
month: 'short',
|
|
62
|
+
day: 'numeric',
|
|
63
|
+
hour: 'numeric',
|
|
64
|
+
minute: '2-digit'
|
|
65
|
+
}).format(new Date(value));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function getVisibleTodos(items: TodoItem[], filter: TodoFilter, query: string): TodoItem[] {
|
|
69
|
+
const search = query.trim().toLowerCase();
|
|
70
|
+
|
|
71
|
+
return [...items]
|
|
72
|
+
.filter((todo) => {
|
|
73
|
+
if (filter === 'active' && todo.completed) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (filter === 'completed' && !todo.completed) {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!search) {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return `${todo.title} ${todo.notes}`.toLowerCase().includes(search);
|
|
86
|
+
})
|
|
87
|
+
.sort((left, right) =>
|
|
88
|
+
Number(left.completed) - Number(right.completed)
|
|
89
|
+
|| priorityWeight[left.priority] - priorityWeight[right.priority]
|
|
90
|
+
|| right.updatedAt.localeCompare(left.updatedAt)
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function TodoPage() {
|
|
95
|
+
const todos = createState<TodoItem[]>([]);
|
|
96
|
+
const summary = createState<TodoSummary>(emptySummary);
|
|
97
|
+
const draftTitle = createState('');
|
|
98
|
+
const draftNotes = createState('');
|
|
99
|
+
const draftPriority = createState<TodoPriority>('medium');
|
|
100
|
+
const filter = createState<TodoFilter>('all');
|
|
101
|
+
const search = createState('');
|
|
102
|
+
const isLoading = createState(true);
|
|
103
|
+
const isSaving = createState(false);
|
|
104
|
+
const isClearing = createState(false);
|
|
105
|
+
const error = createState('');
|
|
106
|
+
const notice = createState('');
|
|
107
|
+
let noticeTimer: ReturnType<typeof setTimeout> | null = null;
|
|
108
|
+
|
|
109
|
+
function syncPayload(payload: TodoResponse) {
|
|
110
|
+
todos.value = payload.todos;
|
|
111
|
+
summary.value = payload.summary || summarizeTodos(payload.todos);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function setNotice(message: string) {
|
|
115
|
+
notice.value = message;
|
|
116
|
+
|
|
117
|
+
if (noticeTimer) {
|
|
118
|
+
clearTimeout(noticeTimer);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
noticeTimer = setTimeout(() => {
|
|
122
|
+
notice.value = '';
|
|
123
|
+
}, 2600);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
async function request(url: string, init?: RequestInit): Promise<TodoResponse> {
|
|
127
|
+
const response = await fetch(url, {
|
|
128
|
+
headers: {
|
|
129
|
+
'Content-Type': 'application/json'
|
|
130
|
+
},
|
|
131
|
+
...init
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
const payload = await response.json() as TodoResponse & { error?: string };
|
|
135
|
+
|
|
136
|
+
if (!response.ok) {
|
|
137
|
+
throw new Error(payload.error || 'The todo request failed.');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return payload;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
async function loadTodos() {
|
|
144
|
+
isLoading.value = true;
|
|
145
|
+
error.value = '';
|
|
146
|
+
|
|
147
|
+
try {
|
|
148
|
+
const payload = await request('/api/todos');
|
|
149
|
+
syncPayload(payload);
|
|
150
|
+
} catch (err) {
|
|
151
|
+
error.value = err instanceof Error ? err.message : 'Unable to load tasks.';
|
|
152
|
+
} finally {
|
|
153
|
+
isLoading.value = false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function createTodo(event: Event) {
|
|
158
|
+
event.preventDefault();
|
|
159
|
+
|
|
160
|
+
const title = draftTitle.value.trim();
|
|
161
|
+
const notes = draftNotes.value.trim();
|
|
162
|
+
|
|
163
|
+
if (!title) {
|
|
164
|
+
error.value = 'Add a task title before saving.';
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
isSaving.value = true;
|
|
169
|
+
error.value = '';
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
const payload = await request('/api/todos', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify({
|
|
175
|
+
title,
|
|
176
|
+
notes,
|
|
177
|
+
priority: draftPriority.value
|
|
178
|
+
})
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
syncPayload(payload);
|
|
182
|
+
draftTitle.value = '';
|
|
183
|
+
draftNotes.value = '';
|
|
184
|
+
draftPriority.value = 'medium';
|
|
185
|
+
setNotice(payload.message || 'Task added.');
|
|
186
|
+
} catch (err) {
|
|
187
|
+
error.value = err instanceof Error ? err.message : 'Unable to save the task.';
|
|
188
|
+
} finally {
|
|
189
|
+
isSaving.value = false;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function toggleTodo(todo: TodoItem) {
|
|
194
|
+
error.value = '';
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
const payload = await request(`/api/todos/${todo.id}`, {
|
|
198
|
+
method: 'PATCH',
|
|
199
|
+
body: JSON.stringify({
|
|
200
|
+
completed: !todo.completed
|
|
201
|
+
})
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
syncPayload(payload);
|
|
205
|
+
setNotice(payload.message || 'Task updated.');
|
|
206
|
+
} catch (err) {
|
|
207
|
+
error.value = err instanceof Error ? err.message : 'Unable to update the task.';
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function deleteTodo(todo: TodoItem) {
|
|
212
|
+
error.value = '';
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
const payload = await request(`/api/todos/${todo.id}`, {
|
|
216
|
+
method: 'DELETE'
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
syncPayload(payload);
|
|
220
|
+
setNotice(payload.message || 'Task removed.');
|
|
221
|
+
} catch (err) {
|
|
222
|
+
error.value = err instanceof Error ? err.message : 'Unable to remove the task.';
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function clearCompleted() {
|
|
227
|
+
if (!summary.value.completed) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
isClearing.value = true;
|
|
232
|
+
error.value = '';
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const payload = await request('/api/todos/completed', {
|
|
236
|
+
method: 'DELETE'
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
syncPayload(payload);
|
|
240
|
+
setNotice(payload.message || 'Completed tasks cleared.');
|
|
241
|
+
} catch (err) {
|
|
242
|
+
error.value = err instanceof Error ? err.message : 'Unable to clear completed tasks.';
|
|
243
|
+
} finally {
|
|
244
|
+
isClearing.value = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
void loadTodos();
|
|
249
|
+
|
|
250
|
+
return div({ className: 'todo-page' },
|
|
251
|
+
section({ className: 'todo-panel todo-hero' },
|
|
252
|
+
div({ className: 'todo-hero-copy' },
|
|
253
|
+
span({ className: 'todo-kicker' }, 'Todo Fullstack Starter'),
|
|
254
|
+
h1({ className: 'todo-headline' }, 'Small tasks. Clear ownership. Visible progress.'),
|
|
255
|
+
p({ className: 'todo-description' },
|
|
256
|
+
'This starter gives you a real CRUD board with API routes and a file-backed database. Add tasks, complete them, clear the finished work, and watch every change persist to ',
|
|
257
|
+
span({ className: 'storage-tag' }, 'databases/todo.ts'),
|
|
258
|
+
'.'
|
|
259
|
+
),
|
|
260
|
+
div({ className: 'todo-hero-actions' },
|
|
261
|
+
button({ className: 'btn btn-primary', type: 'button', onclick: () => { void loadTodos(); } }, 'Refresh board'),
|
|
262
|
+
span({ className: 'storage-tag' }, 'Powered by elit/database')
|
|
263
|
+
)
|
|
264
|
+
),
|
|
265
|
+
reactive(summary, (stats) =>
|
|
266
|
+
div({ className: 'todo-hero-card' },
|
|
267
|
+
div(
|
|
268
|
+
h2({ className: 'todo-hero-card-title' }, 'Current snapshot'),
|
|
269
|
+
p({ className: 'todo-hero-card-text' }, 'Keep the queue short, surface the real blockers, and use the data file as a simple local source of truth while you shape the app.')
|
|
270
|
+
),
|
|
271
|
+
div({ className: 'hero-metrics' },
|
|
272
|
+
div({ className: 'hero-metric' },
|
|
273
|
+
span({ className: 'hero-metric-value' }, String(stats.total)),
|
|
274
|
+
span({ className: 'hero-metric-label' }, 'Total tasks')
|
|
275
|
+
),
|
|
276
|
+
div({ className: 'hero-metric' },
|
|
277
|
+
span({ className: 'hero-metric-value' }, String(stats.active)),
|
|
278
|
+
span({ className: 'hero-metric-label' }, 'Open now')
|
|
279
|
+
),
|
|
280
|
+
div({ className: 'hero-metric' },
|
|
281
|
+
span({ className: 'hero-metric-value' }, String(stats.highPriority)),
|
|
282
|
+
span({ className: 'hero-metric-label' }, 'High priority')
|
|
283
|
+
)
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
)
|
|
287
|
+
),
|
|
288
|
+
|
|
289
|
+
div({ className: 'todo-workspace' },
|
|
290
|
+
section({ className: 'todo-panel' },
|
|
291
|
+
h2({ className: 'todo-section-title' }, 'Compose a new task'),
|
|
292
|
+
p({ className: 'todo-section-copy' }, 'Use the form below to add work directly into the local database file. Keep titles sharp and notes short.'),
|
|
293
|
+
form({ className: 'todo-form', onsubmit: createTodo },
|
|
294
|
+
div({ className: 'todo-field' },
|
|
295
|
+
label({ className: 'todo-label', htmlFor: 'todo-title' }, 'Task title'),
|
|
296
|
+
input({
|
|
297
|
+
id: 'todo-title',
|
|
298
|
+
className: 'todo-input',
|
|
299
|
+
placeholder: 'Prepare the stakeholder demo',
|
|
300
|
+
...bindValue(draftTitle)
|
|
301
|
+
})
|
|
302
|
+
),
|
|
303
|
+
div({ className: 'todo-field' },
|
|
304
|
+
label({ className: 'todo-label', htmlFor: 'todo-notes' }, 'Notes'),
|
|
305
|
+
textarea({
|
|
306
|
+
id: 'todo-notes',
|
|
307
|
+
className: 'todo-input todo-textarea',
|
|
308
|
+
placeholder: 'Add delivery notes, constraints, or handoff details.',
|
|
309
|
+
...bindValue(draftNotes)
|
|
310
|
+
})
|
|
311
|
+
),
|
|
312
|
+
div({ className: 'todo-field-row' },
|
|
313
|
+
div({ className: 'todo-field' },
|
|
314
|
+
label({ className: 'todo-label', htmlFor: 'todo-priority' }, 'Priority'),
|
|
315
|
+
select({
|
|
316
|
+
id: 'todo-priority',
|
|
317
|
+
className: 'todo-input',
|
|
318
|
+
...bindValue(draftPriority)
|
|
319
|
+
},
|
|
320
|
+
option({ value: 'high' }, 'High'),
|
|
321
|
+
option({ value: 'medium' }, 'Medium'),
|
|
322
|
+
option({ value: 'low' }, 'Low')
|
|
323
|
+
)
|
|
324
|
+
)
|
|
325
|
+
),
|
|
326
|
+
div({ className: 'todo-submit-row' },
|
|
327
|
+
p({ className: 'todo-hint' }, 'Every successful action updates the TypeScript file in the databases folder, so the starter stays inspectable and easy to debug.'),
|
|
328
|
+
reactive(isSaving, (saving) =>
|
|
329
|
+
button({
|
|
330
|
+
className: 'btn btn-primary',
|
|
331
|
+
type: 'submit',
|
|
332
|
+
disabled: saving
|
|
333
|
+
}, saving ? 'Saving task...' : 'Add task')
|
|
334
|
+
)
|
|
335
|
+
)
|
|
336
|
+
)
|
|
337
|
+
),
|
|
338
|
+
|
|
339
|
+
section({ className: 'todo-panel' },
|
|
340
|
+
h2({ className: 'todo-section-title' }, 'Shape the queue'),
|
|
341
|
+
p({ className: 'todo-section-copy' }, 'Filter the board, keep an eye on high-priority work, and use this side panel as the place to add team-specific rules later.'),
|
|
342
|
+
reactive(summary, (stats) =>
|
|
343
|
+
div({ className: 'summary-grid' },
|
|
344
|
+
div({ className: 'summary-card' },
|
|
345
|
+
span({ className: 'summary-value' }, String(stats.active)),
|
|
346
|
+
span({ className: 'summary-label' }, 'Need attention')
|
|
347
|
+
),
|
|
348
|
+
div({ className: 'summary-card' },
|
|
349
|
+
span({ className: 'summary-value' }, String(stats.completed)),
|
|
350
|
+
span({ className: 'summary-label' }, 'Already done')
|
|
351
|
+
),
|
|
352
|
+
div({ className: 'summary-card' },
|
|
353
|
+
span({ className: 'summary-value' }, String(stats.highPriority)),
|
|
354
|
+
span({ className: 'summary-label' }, 'Ship-first items')
|
|
355
|
+
)
|
|
356
|
+
)
|
|
357
|
+
),
|
|
358
|
+
p({ className: 'filter-label' }, 'Board filter'),
|
|
359
|
+
reactive(filter, (activeFilter) =>
|
|
360
|
+
div({ className: 'filter-group' },
|
|
361
|
+
button({
|
|
362
|
+
className: `filter-chip ${activeFilter === 'all' ? 'filter-chip-active' : ''}`,
|
|
363
|
+
type: 'button',
|
|
364
|
+
onclick: () => {
|
|
365
|
+
filter.value = 'all';
|
|
366
|
+
}
|
|
367
|
+
}, 'All tasks'),
|
|
368
|
+
button({
|
|
369
|
+
className: `filter-chip ${activeFilter === 'active' ? 'filter-chip-active' : ''}`,
|
|
370
|
+
type: 'button',
|
|
371
|
+
onclick: () => {
|
|
372
|
+
filter.value = 'active';
|
|
373
|
+
}
|
|
374
|
+
}, 'Open'),
|
|
375
|
+
button({
|
|
376
|
+
className: `filter-chip ${activeFilter === 'completed' ? 'filter-chip-active' : ''}`,
|
|
377
|
+
type: 'button',
|
|
378
|
+
onclick: () => {
|
|
379
|
+
filter.value = 'completed';
|
|
380
|
+
}
|
|
381
|
+
}, 'Completed')
|
|
382
|
+
)
|
|
383
|
+
)
|
|
384
|
+
)
|
|
385
|
+
),
|
|
386
|
+
|
|
387
|
+
reactive(error, (message) => message
|
|
388
|
+
? div({ className: 'todo-banner todo-banner-error' }, message)
|
|
389
|
+
: null
|
|
390
|
+
),
|
|
391
|
+
|
|
392
|
+
reactive(notice, (message) => message
|
|
393
|
+
? div({ className: 'todo-banner todo-banner-success' }, message)
|
|
394
|
+
: null
|
|
395
|
+
),
|
|
396
|
+
|
|
397
|
+
section({ className: 'todo-panel' },
|
|
398
|
+
div({ className: 'todo-board-toolbar' },
|
|
399
|
+
div({ className: 'todo-search-wrap' },
|
|
400
|
+
input({
|
|
401
|
+
className: 'todo-input',
|
|
402
|
+
placeholder: 'Search title or notes',
|
|
403
|
+
...bindValue(search)
|
|
404
|
+
})
|
|
405
|
+
),
|
|
406
|
+
div({ className: 'todo-toolbar-actions' },
|
|
407
|
+
p({ className: 'todo-toolbar-note' }, 'Sorted by completion state, priority, and latest update.'),
|
|
408
|
+
reactive(isClearing, (clearing) =>
|
|
409
|
+
reactive(summary, (stats) =>
|
|
410
|
+
button({
|
|
411
|
+
className: 'btn btn-secondary',
|
|
412
|
+
type: 'button',
|
|
413
|
+
disabled: clearing || !stats.completed,
|
|
414
|
+
onclick: () => {
|
|
415
|
+
void clearCompleted();
|
|
416
|
+
}
|
|
417
|
+
}, clearing ? 'Clearing...' : stats.completed ? `Clear ${stats.completed} done` : 'Nothing to clear')
|
|
418
|
+
)
|
|
419
|
+
)
|
|
420
|
+
)
|
|
421
|
+
),
|
|
422
|
+
reactive(isLoading, (loading) => {
|
|
423
|
+
if (loading) {
|
|
424
|
+
return div({ className: 'todo-empty' },
|
|
425
|
+
div({ className: 'todo-empty-mark' }, '...'),
|
|
426
|
+
h3({ className: 'todo-empty-title' }, 'Loading the board'),
|
|
427
|
+
p({ className: 'todo-empty-copy' }, 'Reading your starter tasks from databases/todo.ts.')
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return reactive(todos, (items) =>
|
|
432
|
+
reactive(filter, (activeFilter) =>
|
|
433
|
+
reactive(search, (query) => {
|
|
434
|
+
const visibleTodos = getVisibleTodos(items, activeFilter, query);
|
|
435
|
+
|
|
436
|
+
if (!visibleTodos.length) {
|
|
437
|
+
return div({ className: 'todo-empty' },
|
|
438
|
+
div({ className: 'todo-empty-mark' }, '0'),
|
|
439
|
+
h3({ className: 'todo-empty-title' }, 'No tasks match this view'),
|
|
440
|
+
p({ className: 'todo-empty-copy' }, 'Try another filter, clear the search query, or create a new task to repopulate the board.')
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
return div({ className: 'todo-list' },
|
|
445
|
+
...visibleTodos.map((todo) =>
|
|
446
|
+
article({ className: `todo-card ${todo.completed ? 'todo-card-done' : ''}` },
|
|
447
|
+
div({ className: 'todo-card-main' },
|
|
448
|
+
button({
|
|
449
|
+
className: `todo-check ${todo.completed ? 'todo-check-active' : ''}`,
|
|
450
|
+
type: 'button',
|
|
451
|
+
onclick: () => {
|
|
452
|
+
void toggleTodo(todo);
|
|
453
|
+
}
|
|
454
|
+
}, todo.completed ? 'OK' : ''),
|
|
455
|
+
div({ className: 'todo-card-copy' },
|
|
456
|
+
div({ className: 'todo-card-meta' },
|
|
457
|
+
span({ className: `todo-priority todo-priority-${todo.priority}` }, priorityLabel[todo.priority]),
|
|
458
|
+
span({ className: 'todo-date' }, formatDate(todo.updatedAt))
|
|
459
|
+
),
|
|
460
|
+
h3({ className: 'todo-card-title' }, todo.title),
|
|
461
|
+
todo.notes
|
|
462
|
+
? p({ className: 'todo-card-notes' }, todo.notes)
|
|
463
|
+
: p({ className: 'todo-card-notes todo-card-notes-muted' }, 'No extra notes yet. Add context only when it helps handoff.')
|
|
464
|
+
)
|
|
465
|
+
),
|
|
466
|
+
div({ className: 'todo-card-actions' },
|
|
467
|
+
button({
|
|
468
|
+
className: 'btn btn-ghost',
|
|
469
|
+
type: 'button',
|
|
470
|
+
onclick: () => {
|
|
471
|
+
void toggleTodo(todo);
|
|
472
|
+
}
|
|
473
|
+
}, todo.completed ? 'Reopen' : 'Complete'),
|
|
474
|
+
button({
|
|
475
|
+
className: 'btn btn-danger',
|
|
476
|
+
type: 'button',
|
|
477
|
+
onclick: () => {
|
|
478
|
+
void deleteTodo(todo);
|
|
479
|
+
}
|
|
480
|
+
}, 'Delete')
|
|
481
|
+
)
|
|
482
|
+
)
|
|
483
|
+
)
|
|
484
|
+
);
|
|
485
|
+
})
|
|
486
|
+
)
|
|
487
|
+
);
|
|
488
|
+
})
|
|
489
|
+
)
|
|
490
|
+
);
|
|
491
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { createRouter, createRouterView } from 'elit';
|
|
2
|
+
import { TodoPage } from './pages/TodoPage';
|
|
3
|
+
|
|
4
|
+
// Initialize router
|
|
5
|
+
export const router = createRouter({
|
|
6
|
+
mode: 'hash',
|
|
7
|
+
base: '/',
|
|
8
|
+
routes: []
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
// Define routes
|
|
12
|
+
const routes = [
|
|
13
|
+
{ path: '/', component: () => TodoPage() }
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
export const RouterView = createRouterView(router, { mode: 'hash', routes });
|