vite-plugin-server-actions 0.1.1 → 1.0.0
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/LICENSE +21 -0
- package/README.md +606 -105
- package/index.d.ts +200 -0
- package/package.json +74 -11
- package/src/build-utils.js +101 -0
- package/src/index.js +531 -58
- package/src/middleware.js +89 -0
- package/src/openapi.js +369 -0
- package/src/types.ts +35 -0
- package/src/validation.js +307 -0
- package/.editorconfig +0 -20
- package/.nvmrc +0 -1
- package/.prettierrc +0 -7
- package/examples/todo-app/.prettierrc +0 -17
- package/examples/todo-app/README.md +0 -58
- package/examples/todo-app/index.html +0 -12
- package/examples/todo-app/jsconfig.json +0 -32
- package/examples/todo-app/package.json +0 -22
- package/examples/todo-app/src/App.svelte +0 -155
- package/examples/todo-app/src/actions/auth.server.js +0 -14
- package/examples/todo-app/src/actions/todo.server.js +0 -57
- package/examples/todo-app/src/app.css +0 -0
- package/examples/todo-app/src/main.js +0 -8
- package/examples/todo-app/svelte.config.js +0 -7
- package/examples/todo-app/todos.json +0 -27
- package/examples/todo-app/vite.config.js +0 -12
- package/examples/todo-app/yarn.lock +0 -658
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { extendZodWithOpenApi, OpenAPIRegistry, OpenApiGeneratorV3 } from "@asteasolutions/zod-to-openapi";
|
|
3
|
+
|
|
4
|
+
// Extend Zod with OpenAPI support
|
|
5
|
+
extendZodWithOpenApi(z);
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Base validation adapter interface
|
|
9
|
+
*/
|
|
10
|
+
export class ValidationAdapter {
|
|
11
|
+
/**
|
|
12
|
+
* Validate data against a schema
|
|
13
|
+
* @param {any} schema - The validation schema
|
|
14
|
+
* @param {any} data - The data to validate
|
|
15
|
+
* @returns {Promise<{success: boolean, data?: any, errors?: any[]}>}
|
|
16
|
+
*/
|
|
17
|
+
async validate(schema, data) {
|
|
18
|
+
throw new Error("ValidationAdapter.validate must be implemented");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Convert schema to OpenAPI schema format
|
|
23
|
+
* @param {any} schema - The validation schema
|
|
24
|
+
* @returns {object} OpenAPI schema object
|
|
25
|
+
*/
|
|
26
|
+
toOpenAPISchema(schema) {
|
|
27
|
+
throw new Error("ValidationAdapter.toOpenAPISchema must be implemented");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Get parameter definitions for OpenAPI
|
|
32
|
+
* @param {any} schema - The validation schema
|
|
33
|
+
* @returns {Array} Array of OpenAPI parameter objects
|
|
34
|
+
*/
|
|
35
|
+
getParameters(schema) {
|
|
36
|
+
throw new Error("ValidationAdapter.getParameters must be implemented");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Zod validation adapter
|
|
42
|
+
*/
|
|
43
|
+
export class ZodAdapter extends ValidationAdapter {
|
|
44
|
+
async validate(schema, data) {
|
|
45
|
+
try {
|
|
46
|
+
const validatedData = await schema.parseAsync(data);
|
|
47
|
+
return {
|
|
48
|
+
success: true,
|
|
49
|
+
data: validatedData,
|
|
50
|
+
};
|
|
51
|
+
} catch (error) {
|
|
52
|
+
if (error instanceof z.ZodError) {
|
|
53
|
+
return {
|
|
54
|
+
success: false,
|
|
55
|
+
errors: error.errors.map((err) => ({
|
|
56
|
+
path: err.path.join("."),
|
|
57
|
+
message: err.message,
|
|
58
|
+
code: err.code,
|
|
59
|
+
value: err.input,
|
|
60
|
+
})),
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
errors: [
|
|
66
|
+
{
|
|
67
|
+
path: "root",
|
|
68
|
+
message: error.message,
|
|
69
|
+
code: "unknown",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
toOpenAPISchema(schema) {
|
|
77
|
+
try {
|
|
78
|
+
// Use @asteasolutions/zod-to-openapi for conversion
|
|
79
|
+
const registry = new OpenAPIRegistry();
|
|
80
|
+
const schemaName = "_TempSchema";
|
|
81
|
+
|
|
82
|
+
// The library requires schemas to be registered with openapi metadata
|
|
83
|
+
// For simple conversion, we'll create a temporary registry
|
|
84
|
+
const extendedSchema = schema.openapi ? schema : schema;
|
|
85
|
+
registry.register(schemaName, extendedSchema);
|
|
86
|
+
|
|
87
|
+
// Generate the OpenAPI components
|
|
88
|
+
const generator = new OpenApiGeneratorV3(registry.definitions);
|
|
89
|
+
const components = generator.generateComponents();
|
|
90
|
+
|
|
91
|
+
// Extract the schema from components
|
|
92
|
+
const openAPISchema = components.components?.schemas?.[schemaName];
|
|
93
|
+
|
|
94
|
+
if (!openAPISchema) {
|
|
95
|
+
// Fallback for schemas that couldn't be converted
|
|
96
|
+
return { type: "object", description: "Schema conversion not supported" };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return openAPISchema;
|
|
100
|
+
} catch (error) {
|
|
101
|
+
console.warn(`Failed to convert Zod schema to OpenAPI: ${error.message}`);
|
|
102
|
+
return { type: "object", description: "Schema conversion failed" };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
getParameters(schema) {
|
|
107
|
+
if (!schema || typeof schema._def === "undefined") {
|
|
108
|
+
return [];
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// For function parameters, we expect an array schema or object schema
|
|
112
|
+
if (schema._def.typeName === "ZodArray") {
|
|
113
|
+
// Array of parameters - convert each item to a parameter
|
|
114
|
+
const itemSchema = schema._def.type;
|
|
115
|
+
return this._schemaToParameters(itemSchema, "body");
|
|
116
|
+
} else if (schema._def.typeName === "ZodObject") {
|
|
117
|
+
// Object parameters - convert each property to a parameter
|
|
118
|
+
return this._objectToParameters(schema);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return [
|
|
122
|
+
{
|
|
123
|
+
name: "data",
|
|
124
|
+
in: "body",
|
|
125
|
+
required: true,
|
|
126
|
+
schema: this.toOpenAPISchema(schema),
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
_objectToParameters(zodObject) {
|
|
132
|
+
const shape = zodObject._def.shape();
|
|
133
|
+
const parameters = [];
|
|
134
|
+
|
|
135
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
136
|
+
parameters.push({
|
|
137
|
+
name: key,
|
|
138
|
+
in: "body",
|
|
139
|
+
required: !value.isOptional(),
|
|
140
|
+
schema: this.toOpenAPISchema(value),
|
|
141
|
+
description: value.description || `Parameter: ${key}`,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return parameters;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_schemaToParameters(schema, location = "body") {
|
|
149
|
+
return [
|
|
150
|
+
{
|
|
151
|
+
name: "data",
|
|
152
|
+
in: location,
|
|
153
|
+
required: true,
|
|
154
|
+
schema: this.toOpenAPISchema(schema),
|
|
155
|
+
},
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Schema discovery utilities
|
|
162
|
+
*/
|
|
163
|
+
export class SchemaDiscovery {
|
|
164
|
+
constructor(adapter = new ZodAdapter()) {
|
|
165
|
+
this.adapter = adapter;
|
|
166
|
+
this.schemas = new Map();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Register a schema for a function
|
|
171
|
+
* @param {string} moduleName - Module name
|
|
172
|
+
* @param {string} functionName - Function name
|
|
173
|
+
* @param {any} schema - Validation schema
|
|
174
|
+
*/
|
|
175
|
+
registerSchema(moduleName, functionName, schema) {
|
|
176
|
+
const key = `${moduleName}.${functionName}`;
|
|
177
|
+
this.schemas.set(key, schema);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get schema for a function
|
|
182
|
+
* @param {string} moduleName - Module name
|
|
183
|
+
* @param {string} functionName - Function name
|
|
184
|
+
* @returns {any|null} Schema or null if not found
|
|
185
|
+
*/
|
|
186
|
+
getSchema(moduleName, functionName) {
|
|
187
|
+
const key = `${moduleName}.${functionName}`;
|
|
188
|
+
return this.schemas.get(key) || null;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Get all schemas
|
|
193
|
+
* @returns {Map} All registered schemas
|
|
194
|
+
*/
|
|
195
|
+
getAllSchemas() {
|
|
196
|
+
return new Map(this.schemas);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Discover schemas from module exports
|
|
201
|
+
* @param {object} module - Module with exported functions
|
|
202
|
+
* @param {string} moduleName - Module name
|
|
203
|
+
*/
|
|
204
|
+
discoverFromModule(module, moduleName) {
|
|
205
|
+
for (const [functionName, fn] of Object.entries(module)) {
|
|
206
|
+
if (typeof fn === "function") {
|
|
207
|
+
// Check if function has attached schema
|
|
208
|
+
if (fn.schema) {
|
|
209
|
+
this.registerSchema(moduleName, functionName, fn.schema);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Check for JSDoc schema annotations (future enhancement)
|
|
213
|
+
// This would require parsing JSDoc comments from the function
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Clear all schemas
|
|
220
|
+
*/
|
|
221
|
+
clear() {
|
|
222
|
+
this.schemas.clear();
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Validation middleware factory
|
|
228
|
+
*/
|
|
229
|
+
export function createValidationMiddleware(options = {}) {
|
|
230
|
+
const adapter = options.adapter || new ZodAdapter();
|
|
231
|
+
const schemaDiscovery = options.schemaDiscovery || new SchemaDiscovery(adapter);
|
|
232
|
+
|
|
233
|
+
return async function validationMiddleware(req, res, next) {
|
|
234
|
+
let moduleName, functionName, schema;
|
|
235
|
+
|
|
236
|
+
// Check for context from route setup
|
|
237
|
+
if (req.validationContext) {
|
|
238
|
+
moduleName = req.validationContext.moduleName;
|
|
239
|
+
functionName = req.validationContext.functionName;
|
|
240
|
+
schema = req.validationContext.schema;
|
|
241
|
+
} else {
|
|
242
|
+
// Fallback to URL parsing and schema discovery
|
|
243
|
+
const urlParts = req.url.split("/");
|
|
244
|
+
functionName = urlParts[urlParts.length - 1];
|
|
245
|
+
moduleName = urlParts[urlParts.length - 2];
|
|
246
|
+
schema = schemaDiscovery.getSchema(moduleName, functionName);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!schema) {
|
|
250
|
+
// No schema defined, skip validation
|
|
251
|
+
return next();
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Request body should be an array of arguments for server functions
|
|
256
|
+
if (!Array.isArray(req.body) || req.body.length === 0) {
|
|
257
|
+
return res.status(400).json({
|
|
258
|
+
error: "Validation failed",
|
|
259
|
+
details: "Request body must be a non-empty array of function arguments",
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Validate based on schema type
|
|
264
|
+
let validationData;
|
|
265
|
+
if (schema._def?.typeName === "ZodTuple") {
|
|
266
|
+
// Schema expects multiple arguments (tuple)
|
|
267
|
+
validationData = req.body;
|
|
268
|
+
} else {
|
|
269
|
+
// Schema expects single argument (first element of array)
|
|
270
|
+
validationData = req.body[0];
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = await adapter.validate(schema, validationData);
|
|
274
|
+
|
|
275
|
+
if (!result.success) {
|
|
276
|
+
return res.status(400).json({
|
|
277
|
+
error: "Validation failed",
|
|
278
|
+
details: result.errors,
|
|
279
|
+
validationErrors: result.errors,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Replace request body with validated data
|
|
284
|
+
if (schema._def?.typeName === "ZodTuple") {
|
|
285
|
+
req.body = result.data;
|
|
286
|
+
} else {
|
|
287
|
+
req.body = [result.data];
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
next();
|
|
291
|
+
} catch (error) {
|
|
292
|
+
console.error("Validation middleware error:", error);
|
|
293
|
+
res.status(500).json({
|
|
294
|
+
error: "Internal validation error",
|
|
295
|
+
details: error.message,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Export default adapters and utilities
|
|
302
|
+
export const adapters = {
|
|
303
|
+
zod: ZodAdapter,
|
|
304
|
+
};
|
|
305
|
+
|
|
306
|
+
export const defaultAdapter = new ZodAdapter();
|
|
307
|
+
export const defaultSchemaDiscovery = new SchemaDiscovery(defaultAdapter);
|
package/.editorconfig
DELETED
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
root = true
|
|
2
|
-
|
|
3
|
-
[*]
|
|
4
|
-
end_of_line = lf
|
|
5
|
-
insert_final_newline = true
|
|
6
|
-
indent_style = tab
|
|
7
|
-
indent_size = 2
|
|
8
|
-
charset = utf-8
|
|
9
|
-
trim_trailing_whitespace = true
|
|
10
|
-
|
|
11
|
-
[test/**/expected.css]
|
|
12
|
-
insert_final_newline = false
|
|
13
|
-
|
|
14
|
-
[package.json]
|
|
15
|
-
indent_style = space
|
|
16
|
-
|
|
17
|
-
[*.md]
|
|
18
|
-
trim_trailing_whitespace = true
|
|
19
|
-
indent_size = 2
|
|
20
|
-
indent_style = space
|
package/.nvmrc
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
v20.17.0
|
package/.prettierrc
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"],
|
|
3
|
-
"overrides": [
|
|
4
|
-
{
|
|
5
|
-
"files": "*.svelte",
|
|
6
|
-
"options": {
|
|
7
|
-
"parser": "svelte",
|
|
8
|
-
"svelteAllowShorthand": false
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
],
|
|
12
|
-
"printWidth": 120,
|
|
13
|
-
"quoteProps": "consistent",
|
|
14
|
-
"semi": true,
|
|
15
|
-
"singleQuote": false,
|
|
16
|
-
"bracketSameLine": false
|
|
17
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
# Vite Server Actions - TODO App Example
|
|
2
|
-
|
|
3
|
-
This is an example of a simple TODO application that
|
|
4
|
-
uses [Vite Server Actions](https://github.com/HelgeSverre/vite-plugin-server-actions)
|
|
5
|
-
and [Svelte](https://svelte.dev/) to demonstrate a real-world use case, where server actions are used to save, list, and
|
|
6
|
-
delete TODOs, with data stored in a JSON file.
|
|
7
|
-
|
|
8
|
-
## 🚀 Quick Start
|
|
9
|
-
|
|
10
|
-
### Clone the repository and install dependencies
|
|
11
|
-
|
|
12
|
-
```shell
|
|
13
|
-
git clone
|
|
14
|
-
cd examples/todo-app
|
|
15
|
-
npm install
|
|
16
|
-
```
|
|
17
|
-
|
|
18
|
-
## Run in development mode
|
|
19
|
-
|
|
20
|
-
```shell
|
|
21
|
-
npm install
|
|
22
|
-
npm run dev
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
Open [http://localhost:5173](http://localhost:5173) to view it in your browser. (Note: The port may vary, check the
|
|
26
|
-
console output for the correct port)
|
|
27
|
-
|
|
28
|
-
## Build and run in production mode
|
|
29
|
-
|
|
30
|
-
Server Actions works in production mode by bundling the server into a single file that can be run with Node.js.
|
|
31
|
-
|
|
32
|
-
Here's how you can build and run the example in production mode:
|
|
33
|
-
|
|
34
|
-
```shell
|
|
35
|
-
# Install dependencies and build the project
|
|
36
|
-
npm install
|
|
37
|
-
npm run build
|
|
38
|
-
|
|
39
|
-
# Run the generated express.js server
|
|
40
|
-
node dist/server.js
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Open [http://localhost:3000](http://localhost:3000) to view it in your browser.
|
|
44
|
-
|
|
45
|
-
## 📁 Project Structure
|
|
46
|
-
|
|
47
|
-
The important files and directories in this example are:
|
|
48
|
-
|
|
49
|
-
- `src/` - Contains the client-side Svelte application
|
|
50
|
-
- `src/actions/` - Contains the server action files (functions that are run on the server, note the `.server.js` suffix)
|
|
51
|
-
- `src/actions/todo.server.js` - Contains server actions for managing TODOs
|
|
52
|
-
- `src/actions/auth.server.js` - Contains a dummy server action for demonstration purposes
|
|
53
|
-
- `src/App.svelte` - The main Svelte component that imports and calls the server actions to manage TODOs.
|
|
54
|
-
- `vite.config.js` - Vite configuration file that includes the Server Actions plugin `serverActions()`
|
|
55
|
-
- `dist/` - The output directory for the production build.
|
|
56
|
-
- `dist/server.js` - The express server that serves the client-side application and the server actions (automatically
|
|
57
|
-
created by the plugin)
|
|
58
|
-
- `todos.json` - The JSON file where the TODOs are stored (serves the purpose of a simple database for this example)
|
|
@@ -1,12 +0,0 @@
|
|
|
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>TODO Example Svelte</title>
|
|
7
|
-
</head>
|
|
8
|
-
<body>
|
|
9
|
-
<div id="app"></div>
|
|
10
|
-
<script type="module" src="/src/main.js"></script>
|
|
11
|
-
</body>
|
|
12
|
-
</html>
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"moduleResolution": "bundler",
|
|
4
|
-
"target": "ESNext",
|
|
5
|
-
"module": "ESNext",
|
|
6
|
-
/**
|
|
7
|
-
* svelte-preprocess cannot figure out whether you have
|
|
8
|
-
* a value or a type, so tell TypeScript to enforce using
|
|
9
|
-
* `import type` instead of `import` for Types.
|
|
10
|
-
*/
|
|
11
|
-
"verbatimModuleSyntax": true,
|
|
12
|
-
"isolatedModules": true,
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
/**
|
|
15
|
-
* To have warnings / errors of the Svelte compiler at the
|
|
16
|
-
* correct position, enable source maps by default.
|
|
17
|
-
*/
|
|
18
|
-
"sourceMap": true,
|
|
19
|
-
"esModuleInterop": true,
|
|
20
|
-
"skipLibCheck": true,
|
|
21
|
-
/**
|
|
22
|
-
* Typecheck JS in `.svelte` and `.js` files by default.
|
|
23
|
-
* Disable this if you'd like to use dynamic types.
|
|
24
|
-
*/
|
|
25
|
-
"checkJs": true
|
|
26
|
-
},
|
|
27
|
-
/**
|
|
28
|
-
* Use global.d.ts instead of compilerOptions.types
|
|
29
|
-
* to avoid limiting type declarations.
|
|
30
|
-
*/
|
|
31
|
-
"include": ["src/**/*.d.ts", "src/**/*.js", "src/**/*.svelte"]
|
|
32
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "todo-app",
|
|
3
|
-
"version": "0.0.0",
|
|
4
|
-
"private": true,
|
|
5
|
-
"type": "module",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"build": "vite build",
|
|
8
|
-
"dev": "vite",
|
|
9
|
-
"format": "npx prettier --write .",
|
|
10
|
-
"preview": "vite preview",
|
|
11
|
-
"sort": "npx sort-package-json"
|
|
12
|
-
},
|
|
13
|
-
"devDependencies": {
|
|
14
|
-
"@sveltejs/vite-plugin-svelte": "^3.1.1",
|
|
15
|
-
"prettier": "^3.3.3",
|
|
16
|
-
"prettier-plugin-svelte": "^3.2.6",
|
|
17
|
-
"prettier-plugin-tailwindcss": "^0.6.5",
|
|
18
|
-
"svelte": "^4.2.18",
|
|
19
|
-
"vite": "^5.4.1",
|
|
20
|
-
"vite-plugin-inspect": "^0.8.7"
|
|
21
|
-
}
|
|
22
|
-
}
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
<script>
|
|
2
|
-
import { onMount } from "svelte";
|
|
3
|
-
import { addTodo, deleteTodo, getTodos, updateTodo } from "./actions/todo.server.js";
|
|
4
|
-
import { login } from "./actions/auth.server.js";
|
|
5
|
-
|
|
6
|
-
let todos = [];
|
|
7
|
-
let newTodoText = "";
|
|
8
|
-
|
|
9
|
-
onMount(() => {
|
|
10
|
-
login("admin", "admin");
|
|
11
|
-
loadTodos();
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
async function loadTodos() {
|
|
15
|
-
console.log("Loading todos...");
|
|
16
|
-
todos = await getTodos();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
async function handleAddTodo() {
|
|
20
|
-
if (!newTodoText.trim()) return;
|
|
21
|
-
|
|
22
|
-
await addTodo({ text: newTodoText });
|
|
23
|
-
await loadTodos();
|
|
24
|
-
newTodoText = "";
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
async function handleToggleTodo(id) {
|
|
28
|
-
const todo = todos.find((t) => t.id === id);
|
|
29
|
-
if (todo) {
|
|
30
|
-
const updatedTodo = await updateTodo(id, { completed: !todo.completed });
|
|
31
|
-
todos = todos.map((t) => (t.id === id ? updatedTodo : t));
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function handleDeleteTodo(id) {
|
|
36
|
-
await deleteTodo(id);
|
|
37
|
-
await loadTodos();
|
|
38
|
-
}
|
|
39
|
-
</script>
|
|
40
|
-
|
|
41
|
-
<main>
|
|
42
|
-
<h1>Todo List</h1>
|
|
43
|
-
|
|
44
|
-
<form class="todo-form" on:submit|preventDefault={handleAddTodo}>
|
|
45
|
-
<input class="todo-input" bind:value={newTodoText} placeholder="Add a new todo" />
|
|
46
|
-
<button class="todo-button" type="submit">Add</button>
|
|
47
|
-
</form>
|
|
48
|
-
|
|
49
|
-
{#if todos.length > 0}
|
|
50
|
-
<ul class="todo-list">
|
|
51
|
-
{#each todos as todo}
|
|
52
|
-
<li class="todo-item">
|
|
53
|
-
<input type="checkbox" checked={todo.completed} on:change={() => handleToggleTodo(todo.id)} />
|
|
54
|
-
<span class:completed={todo.completed}>{todo.text}</span>
|
|
55
|
-
<button class="delete-button" on:click={() => handleDeleteTodo(todo.id)}>Delete</button>
|
|
56
|
-
</li>
|
|
57
|
-
{/each}
|
|
58
|
-
</ul>
|
|
59
|
-
{/if}
|
|
60
|
-
</main>
|
|
61
|
-
|
|
62
|
-
<style>
|
|
63
|
-
/* Basic global styling */
|
|
64
|
-
main {
|
|
65
|
-
font-family: sans-serif;
|
|
66
|
-
max-width: 600px;
|
|
67
|
-
margin: 2rem auto;
|
|
68
|
-
padding: 1rem;
|
|
69
|
-
background-color: #f8f8f8;
|
|
70
|
-
border-radius: 8px;
|
|
71
|
-
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
h1 {
|
|
75
|
-
text-align: center;
|
|
76
|
-
color: #333;
|
|
77
|
-
margin-bottom: 1rem;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
.todo-form {
|
|
81
|
-
display: flex;
|
|
82
|
-
gap: 0.5rem;
|
|
83
|
-
margin-bottom: 1rem;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
.todo-input {
|
|
87
|
-
flex: 1;
|
|
88
|
-
padding: 0.5rem;
|
|
89
|
-
border: 1px solid #ccc;
|
|
90
|
-
border-radius: 4px;
|
|
91
|
-
font-size: 1rem;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
.todo-button {
|
|
95
|
-
padding: 0.5rem 1rem;
|
|
96
|
-
font-size: 1rem;
|
|
97
|
-
color: white;
|
|
98
|
-
background-color: #333;
|
|
99
|
-
border: none;
|
|
100
|
-
border-radius: 4px;
|
|
101
|
-
cursor: pointer;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
.todo-button:hover {
|
|
105
|
-
background-color: #555;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
.todo-list {
|
|
109
|
-
list-style: none;
|
|
110
|
-
padding: 0;
|
|
111
|
-
margin: 0;
|
|
112
|
-
border-top: 1px solid #e0e0e0;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
.todo-item {
|
|
116
|
-
display: flex;
|
|
117
|
-
align-items: center;
|
|
118
|
-
justify-content: space-between;
|
|
119
|
-
padding: 0.5rem;
|
|
120
|
-
border-bottom: 1px solid #e0e0e0;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.todo-item:last-child {
|
|
124
|
-
border-bottom: none;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
.todo-item input[type="checkbox"] {
|
|
128
|
-
margin-right: 1rem;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.todo-item span {
|
|
132
|
-
flex: 1;
|
|
133
|
-
font-size: 1rem;
|
|
134
|
-
color: #333;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.todo-item span.completed {
|
|
138
|
-
text-decoration: line-through;
|
|
139
|
-
color: #888;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
.delete-button {
|
|
143
|
-
padding: 0.25rem 0.5rem;
|
|
144
|
-
font-size: 0.875rem;
|
|
145
|
-
color: #fff;
|
|
146
|
-
background-color: #cc0000;
|
|
147
|
-
border: none;
|
|
148
|
-
border-radius: 4px;
|
|
149
|
-
cursor: pointer;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
.delete-button:hover {
|
|
153
|
-
background-color: #ff3333;
|
|
154
|
-
}
|
|
155
|
-
</style>
|