diff --git a/.env b/.env index 02ca73d..523890a 100644 --- a/.env +++ b/.env @@ -1,2 +1,3 @@ VITE_API_BASE_URL=https://jsonplaceholder.typicode.com +VITE_API_TODO_URL=http://localhost:8000 VITE_API_TIMEOUT=10000 diff --git a/.env.example b/.env.example index 02ca73d..523890a 100644 --- a/.env.example +++ b/.env.example @@ -1,2 +1,3 @@ VITE_API_BASE_URL=https://jsonplaceholder.typicode.com +VITE_API_TODO_URL=http://localhost:8000 VITE_API_TIMEOUT=10000 diff --git a/data/todos.json b/data/todos.json new file mode 100644 index 0000000..4f5073a --- /dev/null +++ b/data/todos.json @@ -0,0 +1,27 @@ +{ + "statusCards": [ + { + "title": "active", + "id": "1" + }, + { + "title": "inProgress", + "id": "2" + }, + { + "title": "done", + "id": "3" + } + ], + "todos": [ + { + "statusCardId": "2", + "status": "inProgress", + "id": "c9d5", + "title": "an active todo", + "dueDate": "2025-04-06T08:37:00.000Z", + "createdAt": "2025-04-06T08:38:15.586Z", + "updatedAt": "2025-04-06T08:38:15.586Z" + } + ] +} diff --git a/package-lock.json b/package-lock.json index 6c9e001..9c54853 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "axios": "^1.3.2", "bootstrap": "^5.3.1", "bootstrap-icons": "^1.11.2", + "json-server": "^1.0.0-beta.3", "pasoonate": "^1.2.5", "pinia": "^2.0.28", "vue": "^3.3.4", @@ -586,6 +587,12 @@ "node": ">= 8" } }, + "node_modules/@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==", + "license": "MIT" + }, "node_modules/@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -602,6 +609,244 @@ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", "dev": true }, + "node_modules/@tinyhttp/accepts": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/accepts/-/accepts-2.2.3.tgz", + "integrity": "sha512-9pQN6pJAJOU3McmdJWTcyq7LLFW8Lj5q+DadyKcvp+sxMkEpktKX5sbfJgJuOvjk6+1xWl7pe0YL1US1vaO/1w==", + "license": "MIT", + "dependencies": { + "mime": "4.0.4", + "negotiator": "^0.6.3" + }, + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/app": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/app/-/app-2.5.2.tgz", + "integrity": "sha512-DcB3Y8GQppLQlO2VxRYF7LzTEAoZb+VRQXuIsErcu2fNaM1xdx6NQZDso5rlZUiaeg6KYYRfU34N4XkZbv6jSA==", + "license": "MIT", + "dependencies": { + "@tinyhttp/cookie": "2.1.1", + "@tinyhttp/proxy-addr": "2.2.1", + "@tinyhttp/req": "2.2.5", + "@tinyhttp/res": "2.2.5", + "@tinyhttp/router": "2.2.3", + "header-range-parser": "1.1.3", + "regexparam": "^2.0.2" + }, + "engines": { + "node": ">=14.21.3" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/content-disposition": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz", + "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/content-type": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-type/-/content-type-0.1.4.tgz", + "integrity": "sha512-dl6f3SHIJPYbhsW1oXdrqOmLSQF/Ctlv3JnNfXAE22kIP7FosqJHxkz/qj2gv465prG8ODKH5KEyhBkvwrueKQ==", + "license": "MIT", + "engines": { + "node": ">=12.4" + } + }, + "node_modules/@tinyhttp/cookie": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cookie/-/cookie-2.1.1.tgz", + "integrity": "sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + }, + "funding": { + "type": "individual", + "url": "https://github.com/tinyhttp/tinyhttp?sponsor=1" + } + }, + "node_modules/@tinyhttp/cookie-signature": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cookie-signature/-/cookie-signature-2.1.1.tgz", + "integrity": "sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/cors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cors/-/cors-2.0.1.tgz", + "integrity": "sha512-qrmo6WJuaiCzKWagv2yA/kw6hIISfF/hOqPWwmI6w0o8apeTMmRN3DoCFvQ/wNVuWVdU5J4KU7OX8aaSOEq51A==", + "license": "MIT", + "dependencies": { + "@tinyhttp/vary": "^0.1.3" + }, + "engines": { + "node": ">=12.20 || 14.x || >=16" + } + }, + "node_modules/@tinyhttp/encode-url": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/encode-url/-/encode-url-2.1.1.tgz", + "integrity": "sha512-AhY+JqdZ56qV77tzrBm0qThXORbsVjs/IOPgGCS7x/wWnsa/Bx30zDUU/jPAUcSzNOzt860x9fhdGpzdqbUeUw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/etag": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/etag/-/etag-2.1.2.tgz", + "integrity": "sha512-j80fPKimGqdmMh6962y+BtQsnYPVCzZfJw0HXjyH70VaJBHLKGF+iYhcKqzI3yef6QBNa8DKIPsbEYpuwApXTw==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/forwarded": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/forwarded/-/forwarded-2.1.2.tgz", + "integrity": "sha512-9H/eulJ68ElY/+zYpTpNhZ7vxGV+cnwaR6+oQSm7bVgZMyuQfgROW/qvZuhmgDTIxnGMXst+Ba4ij6w6Krcs3w==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/logger": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tinyhttp/logger/-/logger-2.1.0.tgz", + "integrity": "sha512-Ma1fJ9CwUbn9r61/4HW6+nflsVoslpOnCrfQ6UeZq7GGIgwLzofms3HoSVG7M+AyRMJpxlfcDdbH5oFVroDMKA==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.20", + "dayjs": "^1.11.13", + "http-status-emojis": "^2.2.0" + }, + "engines": { + "node": ">=14.18 || >=16.20" + } + }, + "node_modules/@tinyhttp/proxy-addr": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/proxy-addr/-/proxy-addr-2.2.1.tgz", + "integrity": "sha512-BicqMqVI91hHq2BQmnqJUh0FQUnx7DncwSGgu2ghlh+JZG2rHK2ZN/rXkfhrx1rrUw6hnd0L36O8GPMh01+dDQ==", + "license": "MIT", + "dependencies": { + "@tinyhttp/forwarded": "2.1.2", + "ipaddr.js": "^2.2.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/req": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@tinyhttp/req/-/req-2.2.5.tgz", + "integrity": "sha512-trfsXwtmsNjMcGKcLJ+45h912kLRqBQCQD06ams3Tq0kf4gHLxjHjoYOC1Z9yGjOn81XllRx8wqvnvr+Kbe3gw==", + "license": "MIT", + "dependencies": { + "@tinyhttp/accepts": "2.2.3", + "@tinyhttp/type-is": "2.2.4", + "@tinyhttp/url": "2.1.1", + "header-range-parser": "^1.1.3" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/res": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@tinyhttp/res/-/res-2.2.5.tgz", + "integrity": "sha512-yBsqjWygpuKAVz4moWlP4hqzwiDDqfrn2mA0wviJAcgvGiyOErtlQwXY7aj3aPiCpURvxvEFO//Gdy6yV+xEpA==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-disposition": "2.2.2", + "@tinyhttp/cookie": "2.1.1", + "@tinyhttp/cookie-signature": "2.1.1", + "@tinyhttp/encode-url": "2.1.1", + "@tinyhttp/req": "2.2.5", + "@tinyhttp/send": "2.2.3", + "@tinyhttp/vary": "^0.1.3", + "es-escape-html": "^0.1.1", + "mime": "4.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/router": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/router/-/router-2.2.3.tgz", + "integrity": "sha512-O0MQqWV3Vpg/uXsMYg19XsIgOhwjyhTYWh51Qng7bxqXixxx2PEvZWnFjP7c84K7kU/nUX41KpkEBTLnznk9/Q==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/send": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/send/-/send-2.2.3.tgz", + "integrity": "sha512-o4cVHHGQ8WjVBS8UT0EE/2WnjoybrfXikHwsRoNlG1pfrC/Sd01u1N4Te8cOd/9aNGLr4mGxWb5qTm2RRtEi7g==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-type": "^0.1.4", + "@tinyhttp/etag": "2.1.2", + "mime": "4.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/type-is": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/type-is/-/type-is-2.2.4.tgz", + "integrity": "sha512-7F328NheridwjIfefBB2j1PEcKKABpADgv7aCJaE8x8EON77ZFrAkI3Rir7pGjopV7V9MBmW88xUQigBEX2rmQ==", + "license": "MIT", + "dependencies": { + "@tinyhttp/content-type": "^0.1.4", + "mime": "4.0.4" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/url": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/url/-/url-2.1.1.tgz", + "integrity": "sha512-POJeq2GQ5jI7Zrdmj22JqOijB5/GeX+LEX7DUdml1hUnGbJOTWDx7zf2b5cCERj7RoXL67zTgyzVblBJC+NJWg==", + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@tinyhttp/vary": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/vary/-/vary-0.1.3.tgz", + "integrity": "sha512-SoL83sQXAGiHN1jm2VwLUWQSQeDAAl1ywOm6T0b0Cg1CZhVsjoiZadmjhxF6FHCCY7OHHVaLnTgSMxTPIDLxMg==", + "license": "MIT", + "engines": { + "node": ">=12.20" + } + }, "node_modules/@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -991,6 +1236,12 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "license": "MIT" + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -1039,6 +1290,12 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "node_modules/dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==", + "license": "MIT" + }, "node_modules/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -1082,6 +1339,42 @@ "node": ">=6.0.0" } }, + "node_modules/dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "license": "MIT", + "dependencies": { + "type-fest": "^4.18.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dot-prop/node_modules/type-fest": { + "version": "4.39.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz", + "integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==", + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/es-escape-html": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/es-escape-html/-/es-escape-html-0.1.1.tgz", + "integrity": "sha512-yUx1o+8RsG7UlszmYPtks+dm6Lho2m8lgHMOsLJQsFI0R8XwUJwiMhM1M4E/S8QLeGyf6MkDV/pWgjQ0tdTSyQ==", + "license": "MIT", + "engines": { + "node": ">=12.x" + } + }, "node_modules/esbuild": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", @@ -1308,6 +1601,18 @@ "node": ">=0.10.0" } }, + "node_modules/eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + }, + "funding": { + "url": "https://github.com/eta-dev/eta?sponsor=1" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1528,6 +1833,21 @@ "node": ">=8" } }, + "node_modules/header-range-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.3.tgz", + "integrity": "sha512-B9zCFt3jH8g09LR1vHL4pcAn8yMEtlSlOUdQemzHMRKMImNIhhszdeosYFfNW0WXKQtXIlWB+O4owHJKvEJYaA==", + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/http-status-emojis": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http-status-emojis/-/http-status-emojis-2.2.0.tgz", + "integrity": "sha512-ompKtgwpx8ff0hsbpIB7oE4ax1LXoHmftsHHStMELX56ivG3GhofTX8ZHWlUaFKfGjcGjw6G3rPk7dJRXMmbbg==", + "license": "MIT" + }, "node_modules/ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -1570,6 +1890,15 @@ "node": ">=0.8.19" } }, + "node_modules/inflection": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz", + "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -1586,6 +1915,15 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -1677,12 +2015,91 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "node_modules/json-server": { + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/json-server/-/json-server-1.0.0-beta.3.tgz", + "integrity": "sha512-DwE69Ep5ccwIJZBUIWEENC30Yj8bwr4Ax9W9VoIWAYnB8Sj4ReptscO8/DRHv/nXwVlmb3Bk73Ls86+VZdYkkA==", + "license": "SEE LICENSE IN ./LICENSE", + "dependencies": { + "@tinyhttp/app": "^2.4.0", + "@tinyhttp/cors": "^2.0.1", + "@tinyhttp/logger": "^2.0.0", + "chalk": "^5.3.0", + "chokidar": "^4.0.1", + "dot-prop": "^9.0.0", + "eta": "^3.5.0", + "inflection": "^3.0.0", + "json5": "^2.2.3", + "lowdb": "^7.0.1", + "milliparsec": "^4.0.0", + "sirv": "^2.0.4", + "sort-on": "^6.1.0" + }, + "bin": { + "json-server": "lib/bin.js" + }, + "engines": { + "node": ">=18.3" + } + }, + "node_modules/json-server/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/json-server/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/json-server/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -1723,6 +2140,21 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "node_modules/lowdb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "license": "MIT", + "dependencies": { + "steno": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1746,6 +2178,30 @@ "node": ">=12" } }, + "node_modules/milliparsec": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-4.0.0.tgz", + "integrity": "sha512-/wk9d4Z6/9ZvoEH/6BI4TrTCgmkpZPuSRN/6fI9aUHOfXdNTuj/VhLS7d+NqG26bi6L9YmGXutVYvWC8zQ0qtA==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==", + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -1777,6 +2233,15 @@ "node": "*" } }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -1800,6 +2265,15 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -2091,6 +2565,15 @@ "node": ">=8.10.0" } }, + "node_modules/regexparam": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", + "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -2254,6 +2737,35 @@ "node": ">=8" } }, + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "license": "MIT", + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sort-on": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sort-on/-/sort-on-6.1.0.tgz", + "integrity": "sha512-WTECP0nYNWO1n2g5bpsV0yZN9cBmZsF8ThHFbOqVN0HBFRoaQZLLEMvMmJlKHNPYQeVngeI5+jJzIfFqOIo1OA==", + "license": "MIT", + "dependencies": { + "dot-prop": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2270,6 +2782,18 @@ "node": ">=0.10.0" } }, + "node_modules/steno": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/typicode" + } + }, "node_modules/strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -2338,6 +2862,15 @@ "node": ">=8.0" } }, + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -2842,6 +3375,11 @@ "fastq": "^1.6.0" } }, + "@polka/url": { + "version": "1.0.0-next.28", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.28.tgz", + "integrity": "sha512-8LduaNlMZGwdZ6qWrKlfa+2M4gahzFkprZiAt2TF8uS0qQgBizKXpXURqvTJ4WtmupWxaLqjRb2UCTe72mu+Aw==" + }, "@popperjs/core": { "version": "2.11.8", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", @@ -2854,6 +3392,152 @@ "integrity": "sha512-sXo/qW2/pAcmT43VoRKOJbDOfV3cYpq3szSVfIThQXNt+E4DfKj361vaAt3c88U5tPUxzEswam7GW48PJqtKAg==", "dev": true }, + "@tinyhttp/accepts": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/accepts/-/accepts-2.2.3.tgz", + "integrity": "sha512-9pQN6pJAJOU3McmdJWTcyq7LLFW8Lj5q+DadyKcvp+sxMkEpktKX5sbfJgJuOvjk6+1xWl7pe0YL1US1vaO/1w==", + "requires": { + "mime": "4.0.4", + "negotiator": "^0.6.3" + } + }, + "@tinyhttp/app": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/app/-/app-2.5.2.tgz", + "integrity": "sha512-DcB3Y8GQppLQlO2VxRYF7LzTEAoZb+VRQXuIsErcu2fNaM1xdx6NQZDso5rlZUiaeg6KYYRfU34N4XkZbv6jSA==", + "requires": { + "@tinyhttp/cookie": "2.1.1", + "@tinyhttp/proxy-addr": "2.2.1", + "@tinyhttp/req": "2.2.5", + "@tinyhttp/res": "2.2.5", + "@tinyhttp/router": "2.2.3", + "header-range-parser": "1.1.3", + "regexparam": "^2.0.2" + } + }, + "@tinyhttp/content-disposition": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-disposition/-/content-disposition-2.2.2.tgz", + "integrity": "sha512-crXw1txzrS36huQOyQGYFvhTeLeG0Si1xu+/l6kXUVYpE0TjFjEZRqTbuadQLfKGZ0jaI+jJoRyqaWwxOSHW2g==" + }, + "@tinyhttp/content-type": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/content-type/-/content-type-0.1.4.tgz", + "integrity": "sha512-dl6f3SHIJPYbhsW1oXdrqOmLSQF/Ctlv3JnNfXAE22kIP7FosqJHxkz/qj2gv465prG8ODKH5KEyhBkvwrueKQ==" + }, + "@tinyhttp/cookie": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cookie/-/cookie-2.1.1.tgz", + "integrity": "sha512-h/kL9jY0e0Dvad+/QU3efKZww0aTvZJslaHj3JTPmIPC9Oan9+kYqmh3M6L5JUQRuTJYFK2nzgL2iJtH2S+6dA==" + }, + "@tinyhttp/cookie-signature": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cookie-signature/-/cookie-signature-2.1.1.tgz", + "integrity": "sha512-VDsSMY5OJfQJIAtUgeQYhqMPSZptehFSfvEEtxr+4nldPA8IImlp3QVcOVuK985g4AFR4Hl1sCbWCXoqBnVWnw==" + }, + "@tinyhttp/cors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/cors/-/cors-2.0.1.tgz", + "integrity": "sha512-qrmo6WJuaiCzKWagv2yA/kw6hIISfF/hOqPWwmI6w0o8apeTMmRN3DoCFvQ/wNVuWVdU5J4KU7OX8aaSOEq51A==", + "requires": { + "@tinyhttp/vary": "^0.1.3" + } + }, + "@tinyhttp/encode-url": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/encode-url/-/encode-url-2.1.1.tgz", + "integrity": "sha512-AhY+JqdZ56qV77tzrBm0qThXORbsVjs/IOPgGCS7x/wWnsa/Bx30zDUU/jPAUcSzNOzt860x9fhdGpzdqbUeUw==" + }, + "@tinyhttp/etag": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/etag/-/etag-2.1.2.tgz", + "integrity": "sha512-j80fPKimGqdmMh6962y+BtQsnYPVCzZfJw0HXjyH70VaJBHLKGF+iYhcKqzI3yef6QBNa8DKIPsbEYpuwApXTw==" + }, + "@tinyhttp/forwarded": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@tinyhttp/forwarded/-/forwarded-2.1.2.tgz", + "integrity": "sha512-9H/eulJ68ElY/+zYpTpNhZ7vxGV+cnwaR6+oQSm7bVgZMyuQfgROW/qvZuhmgDTIxnGMXst+Ba4ij6w6Krcs3w==" + }, + "@tinyhttp/logger": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@tinyhttp/logger/-/logger-2.1.0.tgz", + "integrity": "sha512-Ma1fJ9CwUbn9r61/4HW6+nflsVoslpOnCrfQ6UeZq7GGIgwLzofms3HoSVG7M+AyRMJpxlfcDdbH5oFVroDMKA==", + "requires": { + "colorette": "^2.0.20", + "dayjs": "^1.11.13", + "http-status-emojis": "^2.2.0" + } + }, + "@tinyhttp/proxy-addr": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/proxy-addr/-/proxy-addr-2.2.1.tgz", + "integrity": "sha512-BicqMqVI91hHq2BQmnqJUh0FQUnx7DncwSGgu2ghlh+JZG2rHK2ZN/rXkfhrx1rrUw6hnd0L36O8GPMh01+dDQ==", + "requires": { + "@tinyhttp/forwarded": "2.1.2", + "ipaddr.js": "^2.2.0" + } + }, + "@tinyhttp/req": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@tinyhttp/req/-/req-2.2.5.tgz", + "integrity": "sha512-trfsXwtmsNjMcGKcLJ+45h912kLRqBQCQD06ams3Tq0kf4gHLxjHjoYOC1Z9yGjOn81XllRx8wqvnvr+Kbe3gw==", + "requires": { + "@tinyhttp/accepts": "2.2.3", + "@tinyhttp/type-is": "2.2.4", + "@tinyhttp/url": "2.1.1", + "header-range-parser": "^1.1.3" + } + }, + "@tinyhttp/res": { + "version": "2.2.5", + "resolved": "https://registry.npmjs.org/@tinyhttp/res/-/res-2.2.5.tgz", + "integrity": "sha512-yBsqjWygpuKAVz4moWlP4hqzwiDDqfrn2mA0wviJAcgvGiyOErtlQwXY7aj3aPiCpURvxvEFO//Gdy6yV+xEpA==", + "requires": { + "@tinyhttp/content-disposition": "2.2.2", + "@tinyhttp/cookie": "2.1.1", + "@tinyhttp/cookie-signature": "2.1.1", + "@tinyhttp/encode-url": "2.1.1", + "@tinyhttp/req": "2.2.5", + "@tinyhttp/send": "2.2.3", + "@tinyhttp/vary": "^0.1.3", + "es-escape-html": "^0.1.1", + "mime": "4.0.4" + } + }, + "@tinyhttp/router": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/router/-/router-2.2.3.tgz", + "integrity": "sha512-O0MQqWV3Vpg/uXsMYg19XsIgOhwjyhTYWh51Qng7bxqXixxx2PEvZWnFjP7c84K7kU/nUX41KpkEBTLnznk9/Q==" + }, + "@tinyhttp/send": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/send/-/send-2.2.3.tgz", + "integrity": "sha512-o4cVHHGQ8WjVBS8UT0EE/2WnjoybrfXikHwsRoNlG1pfrC/Sd01u1N4Te8cOd/9aNGLr4mGxWb5qTm2RRtEi7g==", + "requires": { + "@tinyhttp/content-type": "^0.1.4", + "@tinyhttp/etag": "2.1.2", + "mime": "4.0.4" + } + }, + "@tinyhttp/type-is": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@tinyhttp/type-is/-/type-is-2.2.4.tgz", + "integrity": "sha512-7F328NheridwjIfefBB2j1PEcKKABpADgv7aCJaE8x8EON77ZFrAkI3Rir7pGjopV7V9MBmW88xUQigBEX2rmQ==", + "requires": { + "@tinyhttp/content-type": "^0.1.4", + "mime": "4.0.4" + } + }, + "@tinyhttp/url": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@tinyhttp/url/-/url-2.1.1.tgz", + "integrity": "sha512-POJeq2GQ5jI7Zrdmj22JqOijB5/GeX+LEX7DUdml1hUnGbJOTWDx7zf2b5cCERj7RoXL67zTgyzVblBJC+NJWg==" + }, + "@tinyhttp/vary": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@tinyhttp/vary/-/vary-0.1.3.tgz", + "integrity": "sha512-SoL83sQXAGiHN1jm2VwLUWQSQeDAAl1ywOm6T0b0Cg1CZhVsjoiZadmjhxF6FHCCY7OHHVaLnTgSMxTPIDLxMg==" + }, "@ungap/structured-clone": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", @@ -3158,6 +3842,11 @@ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==" + }, "combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -3194,6 +3883,11 @@ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" }, + "dayjs": { + "version": "1.11.13", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz", + "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==" + }, "debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -3223,6 +3917,26 @@ "esutils": "^2.0.2" } }, + "dot-prop": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz", + "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==", + "requires": { + "type-fest": "^4.18.2" + }, + "dependencies": { + "type-fest": { + "version": "4.39.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.39.1.tgz", + "integrity": "sha512-uW9qzd66uyHYxwyVBYiwS4Oi0qZyUqwjU+Oevr6ZogYiXt99EOYtwvzMSLw1c3lYo2HzJsep/NB23iEVEgjG/w==" + } + } + }, + "es-escape-html": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/es-escape-html/-/es-escape-html-0.1.1.tgz", + "integrity": "sha512-yUx1o+8RsG7UlszmYPtks+dm6Lho2m8lgHMOsLJQsFI0R8XwUJwiMhM1M4E/S8QLeGyf6MkDV/pWgjQ0tdTSyQ==" + }, "esbuild": { "version": "0.16.17", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", @@ -3388,6 +4102,11 @@ "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", "dev": true }, + "eta": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/eta/-/eta-3.5.0.tgz", + "integrity": "sha512-e3x3FBvGzeCIHhF+zhK8FZA2vC5uFn6b4HJjegUbIWrDb4mJ7JjTGMJY9VGIbRVpmSwHopNiaJibhjIr+HfLug==" + }, "fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3548,6 +4267,16 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true }, + "header-range-parser": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/header-range-parser/-/header-range-parser-1.1.3.tgz", + "integrity": "sha512-B9zCFt3jH8g09LR1vHL4pcAn8yMEtlSlOUdQemzHMRKMImNIhhszdeosYFfNW0WXKQtXIlWB+O4owHJKvEJYaA==" + }, + "http-status-emojis": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/http-status-emojis/-/http-status-emojis-2.2.0.tgz", + "integrity": "sha512-ompKtgwpx8ff0hsbpIB7oE4ax1LXoHmftsHHStMELX56ivG3GhofTX8ZHWlUaFKfGjcGjw6G3rPk7dJRXMmbbg==" + }, "ignore": { "version": "5.2.4", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", @@ -3578,6 +4307,11 @@ "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", "dev": true }, + "inflection": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-3.0.2.tgz", + "integrity": "sha512-+Bg3+kg+J6JUWn8J6bzFmOWkTQ6L/NHfDRSYU+EVvuKHDxUDHAXgqixHfVlzuBQaPOTac8hn43aPhMNk6rMe3g==" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -3594,6 +4328,11 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "dev": true }, + "ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==" + }, "is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -3664,12 +4403,57 @@ "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, + "json-server": { + "version": "1.0.0-beta.3", + "resolved": "https://registry.npmjs.org/json-server/-/json-server-1.0.0-beta.3.tgz", + "integrity": "sha512-DwE69Ep5ccwIJZBUIWEENC30Yj8bwr4Ax9W9VoIWAYnB8Sj4ReptscO8/DRHv/nXwVlmb3Bk73Ls86+VZdYkkA==", + "requires": { + "@tinyhttp/app": "^2.4.0", + "@tinyhttp/cors": "^2.0.1", + "@tinyhttp/logger": "^2.0.0", + "chalk": "^5.3.0", + "chokidar": "^4.0.1", + "dot-prop": "^9.0.0", + "eta": "^3.5.0", + "inflection": "^3.0.0", + "json5": "^2.2.3", + "lowdb": "^7.0.1", + "milliparsec": "^4.0.0", + "sirv": "^2.0.4", + "sort-on": "^6.1.0" + }, + "dependencies": { + "chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==" + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "requires": { + "readdirp": "^4.0.1" + } + }, + "readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==" + } + } + }, "json-stable-stringify-without-jsonify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", "dev": true }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, "levn": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", @@ -3701,6 +4485,14 @@ "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", "dev": true }, + "lowdb": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz", + "integrity": "sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==", + "requires": { + "steno": "^4.0.2" + } + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -3718,6 +4510,16 @@ "@jridgewell/sourcemap-codec": "^1.4.15" } }, + "milliparsec": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/milliparsec/-/milliparsec-4.0.0.tgz", + "integrity": "sha512-/wk9d4Z6/9ZvoEH/6BI4TrTCgmkpZPuSRN/6fI9aUHOfXdNTuj/VhLS7d+NqG26bi6L9YmGXutVYvWC8zQ0qtA==" + }, + "mime": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.0.4.tgz", + "integrity": "sha512-v8yqInVjhXyqP6+Kw4fV3ZzeMRqEW6FotRsKXjRS5VMTNIuXsdRoAvklpoRgSqXm6o9VNH4/C0mgedko9DdLsQ==" + }, "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -3740,6 +4542,11 @@ "brace-expansion": "^1.1.7" } }, + "mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3757,6 +4564,11 @@ "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "dev": true }, + "negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==" + }, "normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3937,6 +4749,11 @@ "picomatch": "^2.2.1" } }, + "regexparam": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/regexparam/-/regexparam-2.0.2.tgz", + "integrity": "sha512-A1PeDEYMrkLrfyOwv2jwihXbo9qxdGD3atBYQA9JJgreAx8/7rC6IUkWOw2NQlOxLp2wL0ifQbh1HuidDfYA6w==" + }, "resolve": { "version": "1.22.1", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", @@ -4036,6 +4853,24 @@ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true }, + "sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", + "requires": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + } + }, + "sort-on": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/sort-on/-/sort-on-6.1.0.tgz", + "integrity": "sha512-WTECP0nYNWO1n2g5bpsV0yZN9cBmZsF8ThHFbOqVN0HBFRoaQZLLEMvMmJlKHNPYQeVngeI5+jJzIfFqOIo1OA==", + "requires": { + "dot-prop": "^9.0.0" + } + }, "source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -4046,6 +4881,11 @@ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==" }, + "steno": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/steno/-/steno-4.0.2.tgz", + "integrity": "sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==" + }, "strip-ansi": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", @@ -4093,6 +4933,11 @@ "is-number": "^7.0.0" } }, + "totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", diff --git a/package.json b/package.json index 65ebf18..42ad9a4 100644 --- a/package.json +++ b/package.json @@ -7,12 +7,14 @@ "dev": "vite", "build": "vite build", "preview": "vite preview", - "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore" + "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore", + "server": "json-server --watch data/todos.json --port 8000" }, "dependencies": { "axios": "^1.3.2", "bootstrap": "^5.3.1", "bootstrap-icons": "^1.11.2", + "json-server": "^1.0.0-beta.3", "pasoonate": "^1.2.5", "pinia": "^2.0.28", "vue": "^3.3.4", diff --git a/src/App.vue b/src/App.vue index 5dea8c1..c2ef471 100644 --- a/src/App.vue +++ b/src/App.vue @@ -1,15 +1,17 @@ + \ No newline at end of file + diff --git a/src/components/VPopup.vue b/src/components/VPopup.vue new file mode 100644 index 0000000..e7176c0 --- /dev/null +++ b/src/components/VPopup.vue @@ -0,0 +1,54 @@ + + + + + + + + {{ $t('Confirm') }} + + + + {{ $t('Cancel') }} + + + + + + + diff --git a/src/components/VToasts.vue b/src/components/VToasts.vue index a30ada5..8884680 100644 --- a/src/components/VToasts.vue +++ b/src/components/VToasts.vue @@ -1,5 +1,5 @@ - + - - + + {{ String(Math.ceil(item.timer / 1000)).padStart(2, 0) }} + + + + class="bg-transparent border-0 btn-link text-decoration-none text-info" + @click="undo(item)" + > + Undo + + diff --git a/src/components/comments/CommentsFilter.vue b/src/components/comments/CommentsFilter.vue new file mode 100644 index 0000000..6b1d2d7 --- /dev/null +++ b/src/components/comments/CommentsFilter.vue @@ -0,0 +1,92 @@ + + + + + + {{ $t("Search") }} + + + + + + {{ $t("Body Length") }} + + + + + + {{ $t("Status") }} + + + + + + diff --git a/src/components/data-table/VTable.vue b/src/components/data-table/VTable.vue index 8edfeba..400090c 100644 --- a/src/components/data-table/VTable.vue +++ b/src/components/data-table/VTable.vue @@ -11,6 +11,7 @@ { - const start = (pageRef.value - 1) * itemsPerPageRef.value; - const end = pageRef.value * itemsPerPageRef.value; + if (props.hasPagination) { + const start = (pageRef.value - 1) * itemsPerPageRef.value; + const end = pageRef.value * itemsPerPageRef.value; - return props.items.slice(start, end); + return props.items.slice(start, end); + } else return props.items; }); return { diff --git a/src/components/layout/side-menu/VSideMenu.vue b/src/components/layout/side-menu/VSideMenu.vue index e6b252c..a2efd70 100644 --- a/src/components/layout/side-menu/VSideMenu.vue +++ b/src/components/layout/side-menu/VSideMenu.vue @@ -24,6 +24,22 @@ {{ $t('Todos') }} + + + + + + + {{ $t('Todos Test') }} + + + + + + + + {{ $t('comments') }} + diff --git a/src/components/pagination/VPagination.vue b/src/components/pagination/VPagination.vue index edf7ea8..3d897c5 100644 --- a/src/components/pagination/VPagination.vue +++ b/src/components/pagination/VPagination.vue @@ -1,108 +1,213 @@ - + + + + {{ $t('Previous') }} - - - - {{ $t('From') }} {{ count }} - - {{ $t('Next') }} - - {{ $t('Size') }}: - - - {{ option }} - - + {{ number }} + + + + + + + {{ $t('Size') }}: + + + {{ option }} + + + + diff --git a/src/components/todos-test/addList.vue b/src/components/todos-test/addList.vue new file mode 100644 index 0000000..bf7e73f --- /dev/null +++ b/src/components/todos-test/addList.vue @@ -0,0 +1,72 @@ + + + + {{ $t("Add List") }} + + + + + + + + {{ $t("Add List") }} + + + + + + + + + + diff --git a/src/components/todos-test/addTodo.vue b/src/components/todos-test/addTodo.vue new file mode 100644 index 0000000..8f36cc0 --- /dev/null +++ b/src/components/todos-test/addTodo.vue @@ -0,0 +1,88 @@ + + + + + {{ $t("Add a todo") }} + + + + + + + + + {{ $t("Due Date") }}: + + + + + + {{ $t("Add todo") }} + + + + + + + + + + diff --git a/src/components/todos-test/todoItem.vue b/src/components/todos-test/todoItem.vue new file mode 100644 index 0000000..a14d9c6 --- /dev/null +++ b/src/components/todos-test/todoItem.vue @@ -0,0 +1,63 @@ + + + + {{ todo?.title }} + + + + + + + + + + + diff --git a/src/components/todos-test/todoList.vue b/src/components/todos-test/todoList.vue new file mode 100644 index 0000000..8aac4a7 --- /dev/null +++ b/src/components/todos-test/todoList.vue @@ -0,0 +1,85 @@ + + + + + + + + + + + + + + + + diff --git a/src/components/todos-test/todoModal.vue b/src/components/todos-test/todoModal.vue new file mode 100644 index 0000000..27e2e06 --- /dev/null +++ b/src/components/todos-test/todoModal.vue @@ -0,0 +1,140 @@ + + + + + {{ $t("Todo Details") }} + × + + + + + + {{ $t("Title") }}: {{ currentTodo?.title }} + + + {{ $t("Status") }}: {{ currentTodo?.status }} + + {{ $t('Due') }}: {{ formatDate(currentTodo.dueDate) }} + + + + + + + + + + + diff --git a/src/composables/comments/comments.composable.js b/src/composables/comments/comments.composable.js new file mode 100644 index 0000000..387adcf --- /dev/null +++ b/src/composables/comments/comments.composable.js @@ -0,0 +1,147 @@ +import {ref, nextTick, onMounted, computed} from "vue"; + +import {useCommentsStore} from "@/stores/comments.store"; +import {useToast} from "../toast.composable"; +import {t} from "@/services/language.service"; +import {usePopup} from "@/composables/popup.composable"; + +export default function useComments() { + const {showToast} = useToast(); + const {showPopup} = usePopup(); + const commentsStore = useCommentsStore(); + const selectedItem = ref(null); + const selectedResponse = ref(null); + const responseItems = [ + { + key: "confirmed", + text: t("Confirmed"), + }, + { + key: "rejected", + text: t("Rejected"), + }, + ]; + const relatedPost = computed(() => { + return commentsStore.posts.find(post => post.id === selectedItem.value.postId); + }); + const showInfoModal = ref(false); + const confirm = (item) => { + const foundItem = commentsStore.comments.find((comment) => comment.id === item.id); + foundItem.status = "confirmed"; + }; + + const confirmAll = () => { + + showPopup({ + id: Date.now(), + title: "title", + body: `Are you sure want to confirm all comments?`, + theme: "warning", + confirm: () => { + commentsStore.comments.forEach((comment) => comment.status = "confirmed"); + } + }); + }; + + const reject = (item) => { + const tempItemHolder = {...item}; + let foundItem = commentsStore.comments.find((comment) => comment.id === item.id); + + showPopup({ + id: Date.now(), + title: "title", + body: `Are you sure want to delete comment with id ${item.id} ...`, + theme: "warning", + timer: true, + confirm: () => { + foundItem = commentsStore.comments.find((comment) => comment.id === item.id); + foundItem.status = tempItemHolder.status; + foundItem.status = "rejected"; + + showToast({ + id: Date.now(), + title: "title", + body: `Rejecting comment with id ${item.id} ...`, + theme: "success", + clearable: true, + duration: 10000, + timer: true, + undoAction: () => { + foundItem = commentsStore.comments.find((comment) => comment.id === item.id); + foundItem.status = tempItemHolder.status; + }, + }); + } + }); + + }; + + const rejectAll = () => { + const tempItemHolder = JSON.parse(JSON.stringify(commentsStore.comments)); + + showPopup({ + id: Date.now(), + title: "title", + body: `Are you sure want to delete all comments?`, + theme: "warning", + timer: true, + confirm: () => { + commentsStore.comments.forEach((comment) => comment.status = "rejected"); + + showToast({ + id: Date.now(), + title: "title", + body: `Rejecting all comments ...`, + theme: "success", + clearable: true, + duration: 10000, + timer: true, + undoAction: () => { + commentsStore.comments.forEach((comment, index) => comment.status = tempItemHolder[index].status) + }, + }); + } + }); + + }; + + const handleResponseAllChange = () => { + if (selectedResponse.value === 'confirmed') confirmAll() + else if (selectedResponse.value === 'rejected') rejectAll() + else showToast({ + id: Date.now(), + title: "title", + body: `Choosing Response is required!`, + theme: "danger", + clearable: true, + duration: 5000, + timer: false + }); + } + + const showInfo = (item) => { + selectedItem.value = item; + showInfoModal.value = true; + }; + + + const commentModalTitleText = ((item) => { + return t("Information") + ' ' + t("for") + ' ' + t("comment") + ' ' + t("with") + ' ' + t("ID") + ": " + item?.id + }) + + commentsStore.fetchComments(); + + return { + commentsStore, + confirm, + reject, + showInfo, + showInfoModal, + relatedPost, + commentModalTitleText, + selectedItem, + selectedResponse, + responseItems, + handleResponseAllChange, + }; +} diff --git a/src/composables/popup.composable.js b/src/composables/popup.composable.js new file mode 100644 index 0000000..4eacdea --- /dev/null +++ b/src/composables/popup.composable.js @@ -0,0 +1,64 @@ +import {ref, getCurrentInstance} from "vue"; + +// Utils +import {getUniqueId} from "@/utils"; + +const items = ref([]); + +export function installPopup() { + const instance = getCurrentInstance(); + + if (instance.type.name !== "VPopup") { + throw new Error("installPopup should only be called in the popup component"); + } + + return items; +} + +export function usePopup() { + function showPopup({body, theme, title, confirm, cancel}) { + const id = getUniqueId(); + + items.value.push({ + id, + body, + theme, + title, + confirm, + cancel + }); + + return id; + } + + function hidePopup(id) { + const index = items.value.findIndex(function (item) { + return item.id === id; + }); + + if (index !== -1) { + if (items.value[index]._interval_id) { + clearInterval(items.value[index]._interval_id); + } + + items.value.splice(index, 1); + } + } + + const confirm = (item) => { + if (item.confirm) item.confirm(); + hidePopup(item.id); + }; + + const cancel = (item) => { + if (item.cancel) item.cancel(); + hidePopup(item.id); + }; + + return { + showPopup, + hidePopup, + confirm, + cancel + }; +} diff --git a/src/composables/toast.composable.js b/src/composables/toast.composable.js index 39648d1..422f56d 100644 --- a/src/composables/toast.composable.js +++ b/src/composables/toast.composable.js @@ -1,27 +1,40 @@ -import { ref, getCurrentInstance } from 'vue'; +import { ref, getCurrentInstance } from "vue"; // Utils -import { getUniqueId } from '@/utils'; +import { getUniqueId } from "@/utils"; const items = ref([]); export function installToast() { const instance = getCurrentInstance(); - if (instance.type.name !== 'VToasts') { - throw new Error('installToast should only be called in the Toasts component'); + if (instance.type.name !== "VToasts") { + throw new Error("installToast should only be called in the Toasts component"); } return items; } export function useToast() { - function showToast({ body, theme, duration = 5000, clearable = true , title}) { + function showToast({ body, theme, timer = true, duration = 5000, clearable = true, title, undoAction }) { const id = getUniqueId(); - const _timeOutId = setTimeout(function () { - hideToast(id); - }, duration); + const timerRef = ref(duration); + + let intervalId = null; + + if (timer) { + intervalId = setInterval(() => { + if (timerRef.value > 0) { + timerRef.value -= 1000; + } + + if (timerRef.value <= 0) { + clearInterval(intervalId); + hideToast(id); + } + }, 1000); + } items.value.push({ id, @@ -30,9 +43,17 @@ export function useToast() { theme, clearable, title, - _time_out_id: _timeOutId + undoAction, + timer: timer ? timerRef : false, + _interval_id: intervalId, }); + if (!timer && duration > 0) { + setTimeout(() => { + hideToast(id); + }, duration); + } + return id; } @@ -41,13 +62,25 @@ export function useToast() { return item.id === id; }); - clearTimeout(items.value[index]._time_out_id); + if (index !== -1) { + if (items.value[index]._interval_id) { + clearInterval(items.value[index]._interval_id); + } - items.value.splice(index, 1); + items.value.splice(index, 1); + } } + const undo = (item) => { + if (item.undoAction) { + item.undoAction(); + hideToast(item.id); + } + }; + return { showToast, - hideToast + hideToast, + undo, }; } diff --git a/src/composables/todo.composable.js b/src/composables/todo.composable.js index d4fbcff..e68b7aa 100644 --- a/src/composables/todo.composable.js +++ b/src/composables/todo.composable.js @@ -1,20 +1,20 @@ -import { ref, computed } from 'vue'; +import { ref, computed } from "vue"; // Service -import TodoService from '@/services/todo.service'; +import TodoService from "@/services/todo.service"; // Composables -import { useLoading } from '@/composables/loading.composable'; +import { useLoading } from "@/composables/loading.composable"; // Utils -import { keyBy } from '@/utils'; +import { keyBy } from "@/utils"; export function useFetchTodos() { const { isLoading, startLoading, endLoading } = useLoading(); const todos = ref([]); - const todosKeyById = computed(() => keyBy(todos.value, 'id')); + const todosKeyById = computed(() => keyBy(todos.value, "id")); /** * @param {AxiosRequestConfig} [config] @@ -36,7 +36,7 @@ export function useFetchTodos() { todosLoading: isLoading, todos, todosKeyById, - fetchTodos + fetchTodos, }; } @@ -61,6 +61,6 @@ export function useFetchTodo(initialValue = TodoService.getDefault()) { return { todoLoading: isLoading, todo, - fetchTodoById + fetchTodoById, }; } diff --git a/src/composables/todos-test/addList.composable.js b/src/composables/todos-test/addList.composable.js new file mode 100644 index 0000000..84fd198 --- /dev/null +++ b/src/composables/todos-test/addList.composable.js @@ -0,0 +1,84 @@ +import { ref, nextTick, onMounted } from "vue"; +import { useTodoStore } from "@/stores/todo.store"; +import { useLoading } from "@/composables/loading.composable"; +import TodoTestService from "@/services/todoTest.service"; + +export default function useAddList() { + const todoStore = useTodoStore(); + + const isAddingNewList = ref(false); + const newListTitle = ref(""); + const newListTitleRef = ref(null); + const { startLoading, endLoading } = useLoading(); + + const fetchStatusCards = async () => { + try { + await todoStore.fetchStatusCards(); + todoStore.statusCards.map((card) => ({ + ...card, + isAddingNewTodo: false, + })); + } catch (error) { + console.error(error); + } + }; + + const startAddingNewList = () => { + isAddingNewList.value = true; + newListTitle.value = ""; + + nextTick(() => { + if (newListTitleRef.value) { + newListTitleRef.value.focus(); + } + }); + }; + + const addNewList = async () => { + if (!newListTitle.value.trim()) return; + + const newList = { + title: newListTitle.value, + }; + try { + startLoading(); + TodoTestService.setURL("statusCards"); + + const response = await TodoTestService.post(newList); + todoStore.statusCards.push({ + ...response.data, + isAddingNewTodo: false, + }); + cancelAddingList(); + } catch (error) { + console.error(error); + endLoading(); + } + }; + + const cancelAddingList = () => { + isAddingNewList.value = false; + }; + + const autoResize = (event) => { + const el = event.target; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }; + + onMounted(() => { + fetchStatusCards(); + todoStore.fetchTodos(); + }); + + return { + isAddingNewList, + newListTitle, + newListTitleRef, + fetchStatusCards, + startAddingNewList, + addNewList, + cancelAddingList, + autoResize, + }; +} diff --git a/src/composables/todos-test/addTodo.composable.js b/src/composables/todos-test/addTodo.composable.js new file mode 100644 index 0000000..e1a42bd --- /dev/null +++ b/src/composables/todos-test/addTodo.composable.js @@ -0,0 +1,65 @@ +import {ref, nextTick, onMounted} from "vue"; +import {useTodoStore} from "@/stores/todo.store"; + +export default function useAddTodo() { + const todoStore = useTodoStore(); + + const newTodoTitles = ref([]); + const newDueDate = ref([]); + const addTodoTextRefs = ref([]); + + const startAddingTodo = (index) => { + todoStore.statusCards[index].isAddingNewTodo = true; + newTodoTitles.value[index] = ""; + newDueDate.value[index] = ""; + + nextTick(() => { + if (addTodoTextRefs.value[index]) addTodoTextRefs.value[index].focus(); + }).catch((error) => console.log(error)); + }; + + const addNewTodo = async (index) => { + if (!newTodoTitles.value[index]?.trim()) return; + + const newTodo = { + title: newTodoTitles.value[index], + status: todoStore.statusCards[index].title, + statusCardId: todoStore.statusCards[index].id, + dueDate: newDueDate.value[index] ? new Date(newDueDate.value[index]).toISOString() : null, + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + try { + await todoStore.addTodo(newTodo); + cancelAddingTodo(index); + } catch (error) { + console.error(error); + } + }; + + const cancelAddingTodo = (index) => { + todoStore.statusCards[index].isAddingNewTodo = false; + }; + + const autoResize = (event) => { + const el = event.target; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }; + + onMounted(() => { + todoStore.fetchStatusCards({isAddingNewTodo: false}); + todoStore.fetchTodos(); + }); + + return { + newTodoTitles, + newDueDate, + addTodoTextRefs, + startAddingTodo, + addNewTodo, + cancelAddingTodo, + autoResize, + }; +} diff --git a/src/composables/todos-test/todoItem.composable.js b/src/composables/todos-test/todoItem.composable.js new file mode 100644 index 0000000..0a58e04 --- /dev/null +++ b/src/composables/todos-test/todoItem.composable.js @@ -0,0 +1,31 @@ +import {ref} from "vue"; +import { useTodoStore } from "@/stores/todo.store"; + +export default function useTodoItem(emit) { + const todoStore = useTodoStore(); + const todoModal = ref(null); + const dragItem = ref(null); + + const openTodoModal = (todo) => { + todoModal.value.openModal(todo); + }; + + const getTodosByStatus = (statusCardId) => { + return todoStore.todos.filter((todo) => todo.statusCardId === statusCardId); + }; + + const handleDragStart = (event, todo, listId) => { + dragItem.value = { todo, fromListId: listId }; + event.target.classList.add("dragging"); + emit("drag-start", dragItem.value); + }; + + return { + todoModal, + dragItem, + openTodoModal, + getTodosByStatus, + handleDragStart, + todoStore + }; +} diff --git a/src/composables/todos-test/todoList.composable.js b/src/composables/todos-test/todoList.composable.js new file mode 100644 index 0000000..2b66c8a --- /dev/null +++ b/src/composables/todos-test/todoList.composable.js @@ -0,0 +1,128 @@ +import { ref, nextTick, onMounted } from "vue"; +import { useTodoStore } from "@/stores/todo.store"; +import TodoTestService from "@/services/todoTest.service"; +import { useLoading } from "../loading.composable"; + +export default function useTodosList() { + const todoStore = useTodoStore(); + + const listTitleRefs = ref({}); + const dragItem = ref(null); + const dragOverList = ref(null); + const { startLoading, endLoading } = useLoading(); + + const autoResize = (event) => { + const el = event.target; + el.style.height = "auto"; + el.style.height = `${el.scrollHeight}px`; + }; + + const startEditingList = async (index) => { + todoStore.statusCards.map((card, i) => { + card.isEditing = i === index; + }); + + nextTick(() => { + if (listTitleRefs.value[index]) { + listTitleRefs.value[index].focus(); + listTitleRefs.value[index].select(); + }}).catch((error) => console.log(error)) + } + + const finishEditingList = async (index) => { + if (!todoStore.statusCards[index].isEditing) return; + + const cardId = todoStore.statusCards[index].id; + const newTitle = todoStore.statusCards[index].title; + + try { + startLoading(); + + TodoTestService.setURL("statusCards"); + await TodoTestService.put(cardId, { title: newTitle }); + + const relatedTodos = todoStore.todos.filter((todo) => todo.statusCardId === cardId); + + relatedTodos.map(async (todo) => { + TodoTestService.setURL("todos"); + await TodoTestService.put(todo.id, { ...todo, status: newTitle }); + todo.status = newTitle; + }); + } catch (error) { + console.error(error); + } finally { + todoStore.statusCards[index].isEditing = false; + endLoading(); + } + }; + + const getDraggedItem = (item) => { + dragItem.value = item; + console.log(item); + }; + + const handleDragOver = (event, listId) => { + event.preventDefault(); + event.stopPropagation(); + dragOverList.value = listId; + }; + + const handleDrop = async (event, listId) => { + event.preventDefault(); + event.stopPropagation(); + if (!dragItem.value) return; + + const { todo, fromListId } = dragItem.value; + + if (fromListId === listId) { + resetDragState(); + return; + } + + try { + startLoading(); + + TodoTestService.setURL("todos"); + await TodoTestService.put(todo.id, { + ...todo, + statusCardId: listId, + status: todoStore.statusCards.find((card) => card.id === listId)?.title, + }); + + await todoStore.fetchTodos(); + } catch (error) { + console.error(error); + } finally { + resetDragState(); + endLoading(); + } + }; + + const resetDragState = () => { + document.querySelectorAll(".dragging").forEach((el) => { + el.classList.remove("dragging"); + }); + + dragItem.value = null; + dragOverList.value = null; + }; + + onMounted(() => { + todoStore.fetchTodos(); + todoStore.fetchStatusCards(); + }); + + return { + listTitleRefs, + dragItem, + dragOverList, + autoResize, + startEditingList, + finishEditingList, + getDraggedItem, + handleDragOver, + handleDrop, + resetDragState, + todoStore, + }; +} diff --git a/src/locales/en/messages.js b/src/locales/en/messages.js index c8da1c7..a2e2ea7 100644 --- a/src/locales/en/messages.js +++ b/src/locales/en/messages.js @@ -1,3 +1,5 @@ +// prettier-ignore + export default { "Login": "Login", "Username": "Username", @@ -20,16 +22,51 @@ export default { "From": "From", "Size": "Size", "Todos": "Todos", + "Todos Test": "Todos Test", "Title": "Title", + "Todo Details": "Todo Details", + "Delete Todo": "Delete Todo", + "Add List": "Add List", + "Add a todo": "Add a todo", + "Add todo": "Add todo", + "Due": "Due", + "Due Date": "Due Date", + "pending": "pending", + "Pending": "Pending", + "Body": "Body", + "Show information": "Show information", + "Confirmed": "Confirmed", + "Confirm": "Confirm", + "Rejected": "Rejected", + "Reject": "Reject", + "comments": "comments", + "180 - 210 characters": "180 - 210 characters", + "150 - 180 characters": "150 - 180 characters", + "210 characters or more": "210 characters or more", "User": "User", "Status": "Status", + "Actions": "Actions", + "Body Length": "Body Length", + "Filter": "Filter", + "Filter by Body or Id": "Filter by Body or Id", + "Filter by Body length": "Filter by Body length", + "Filter by Status": "Filter by Status", + "Search": "Search", "Completed": "Completed", "Not completed": "Not completed", + "ID": "ID", + "with": "with", + "for": "for", + "information": "information", + "Information": "Information", + "comment": "comment", "Filter by title": "Filter by title", "Filter by user": "Filter by user", "Filter by status": "Filter by status", "Language": "Language", + "Close": "Close", "Theme": "Theme", + "Response All": "Response All", "Light": "Light", "Dark": "Dark", "System": "System", diff --git a/src/router/index.js b/src/router/index.js index 452ceea..42568bd 100644 --- a/src/router/index.js +++ b/src/router/index.js @@ -33,6 +33,16 @@ const router = createRouter({ path: 'todos', name: 'Todos', component: () => import('@/views/TodosView.vue') + }, + { + path: 'todos-test', + name: 'Todos-Test', + component: () => import('@/views/TodosViewTest.vue') + }, + { + path: 'comments', + name: 'comments', + component: () => import('@/views/CommentsView.vue') } ] }, diff --git a/src/services/api.service.js b/src/services/api.service.js index 047b07a..c6c47d2 100644 --- a/src/services/api.service.js +++ b/src/services/api.service.js @@ -1,7 +1,8 @@ -import axios from 'axios'; - -import HttpMethod from '@/enums/HttpMethod'; +import axios from "axios"; +import { urls } from "./apiUrls.service"; +import HttpMethod from "@/enums/HttpMethod"; +const baseURL = urls.baseUrl; /** * @callback onFulfilledRequest * @param {AxiosRequestConfig} config @@ -13,8 +14,9 @@ import HttpMethod from '@/enums/HttpMethod'; */ const instance = axios.create({ - baseURL: import.meta.env.VITE_API_BASE_URL, - timeout: import.meta.env.VITE_API_TIMEOUT + // baseURL: import.meta.env.VITE_API_BASE_URL, + baseURL, + timeout: import.meta.env.VITE_API_TIMEOUT, }); class ApiService { @@ -66,13 +68,10 @@ class ApiService { * @returns {Number} Middleware id */ static addResponseMiddleware(onFulfilled) { - return instance.interceptors.response.use( - onFulfilled, - function (error) { - onFulfilled(error.response); - return Promise.reject(error); - } - ); + return instance.interceptors.response.use(onFulfilled, function (error) { + onFulfilled(error.response); + return Promise.reject(error); + }); } /** diff --git a/src/services/apiUrls.service.js b/src/services/apiUrls.service.js new file mode 100644 index 0000000..af9dee0 --- /dev/null +++ b/src/services/apiUrls.service.js @@ -0,0 +1,4 @@ +export const urls = { + baseUrl: import.meta.env.VITE_API_BASE_URL, + todoTestUrl: import.meta.env.VITE_API_TODO_URL, +}; diff --git a/src/services/comments.service.js b/src/services/comments.service.js new file mode 100644 index 0000000..eece720 --- /dev/null +++ b/src/services/comments.service.js @@ -0,0 +1,35 @@ +import CrudService from "./crud.service"; + +class TodoTestService extends CrudService { + /** + * Service url + * + * @returns {String} + */ + static URL = "comments"; + + /** + * Set service URL dynamically + * + * @param {String} newUrl + */ + static setURL(newUrl) { + this.URL = newUrl; + } + + /** + * Get default item + * + * @returns {Object} + */ + // static getDefault() { + // return { + // userId: undefined, + // id: undefined, + // title: undefined, + // completed: false + // }; + // } +} + +export default TodoTestService; diff --git a/src/services/crud.service.js b/src/services/crud.service.js index d3e062b..af1786a 100644 --- a/src/services/crud.service.js +++ b/src/services/crud.service.js @@ -1,4 +1,4 @@ -import ApiService from './api.service'; +import ApiService from "./api.service"; class CrudService { /** @@ -8,9 +8,7 @@ class CrudService { * @returns {String} */ static get URL() { - throw new Error( - 'You have to implement the static method "URL", for each class that extend CrudServices!' - ); + throw new Error('You have to implement the static method "URL", for each class that extend CrudServices!'); } /** @@ -45,6 +43,10 @@ class CrudService { return ApiService.post(`${this.URL}/create`, data, config); } + static post(data, config) { + return ApiService.post(`${this.URL}`, data, config); + } + /** * Update item by id * @@ -57,6 +59,10 @@ class CrudService { return ApiService.post(`${this.URL}/${id}/update`, data, config); } + static put(id, data, config) { + return ApiService.put(`${this.URL}/${id}`, data, config); + } + /** * Delete item by id * diff --git a/src/services/todoTest.service.js b/src/services/todoTest.service.js new file mode 100644 index 0000000..f78b84f --- /dev/null +++ b/src/services/todoTest.service.js @@ -0,0 +1,35 @@ +import CrudService from "./crud.service"; + +class TodoTestService extends CrudService { + /** + * Service url + * + * @returns {String} + */ + static URL = "todos"; + + /** + * Set service URL dynamically + * + * @param {String} newUrl + */ + static setURL(newUrl) { + this.URL = newUrl; + } + + /** + * Get default item + * + * @returns {Object} + */ + // static getDefault() { + // return { + // userId: undefined, + // id: undefined, + // title: undefined, + // completed: false + // }; + // } +} + +export default TodoTestService; diff --git a/src/stores/comments.store.js b/src/stores/comments.store.js new file mode 100644 index 0000000..0fffddb --- /dev/null +++ b/src/stores/comments.store.js @@ -0,0 +1,73 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { useLoading } from "@/composables/loading.composable"; +import commentsService from "@/services/comments.service"; + +export const useCommentsStore = defineStore("comments", () => { + const comments = ref(null); + const { isLoading, startLoading, endLoading } = useLoading(); + const posts = ref(false); + + const fetchComments = async (filters) => { + try { + startLoading(); + commentsService.setURL('comments') + const response = await commentsService.getAll(); + let allComments = await response.data; + allComments.forEach((comment, index) => { + if (index % 2 == 0) return (comment.status = "pending"); + else if (index % 3 == 0) return (comment.status = "confirmed"); + else return (comment.status = "rejected"); + }); + + let filteredComments = allComments; + + if (filters.id) { + filteredComments = filteredComments.filter((comment) => { + return comment.id === +filters.id || comment.body.includes(String(filters.id)); + }); + } + + if (filters.length) { + filteredComments = filteredComments.filter((comment) => { + return comment.body.length >= +filters.length && comment.body.length < +filters.length + 30; + }); + } + + if (filters.status) { + filteredComments = filteredComments.filter((comment) => { + return comment.status === filters.status; + }); + } + + comments.value = filteredComments; + + await fetchPosts() + } catch (error) { + console.log(error); + } finally { + endLoading(); + } + }; + + const fetchPosts = async () => { + try { + startLoading(); + commentsService.setURL('posts') + const response = await commentsService.getAll(); + posts.value = await response.data; + } catch (error) { + console.log(error); + } finally { + endLoading(); + } + }; + + return { + comments, + fetchComments, + isLoading, + posts, + fetchPosts + }; +}); diff --git a/src/stores/todo.store.js b/src/stores/todo.store.js new file mode 100644 index 0000000..0d65932 --- /dev/null +++ b/src/stores/todo.store.js @@ -0,0 +1,68 @@ +import { defineStore } from "pinia"; +import { ref } from "vue"; +import { useLoading } from "@/composables/loading.composable"; +import TodoTestService from "@/services/todoTest.service"; + +export const useTodoStore = defineStore("todos", () => { + const statusCards = ref([]); + const todos = ref([]); + const { isLoading, startLoading, endLoading } = useLoading(); + + const fetchStatusCards = async (addingStatus) => { + startLoading(); + + try { + TodoTestService.setURL("statusCards"); + const response = await TodoTestService.getAll(); + statusCards.value = response.data; + + if (addingStatus) { + statusCards.value.map((card) => ({ + ...card, + ...addingStatus, + })); + } + } catch (error) { + console.error(error); + } finally { + endLoading(); + } + }; + + const fetchTodos = async () => { + startLoading(); + + try { + TodoTestService.setURL("todos"); + const response = await TodoTestService.getAll(); + todos.value = response.data; + } catch (error) { + console.error(error); + } finally { + endLoading(); + } + }; + + const addTodo = async (body) => { + startLoading(); + + try { + TodoTestService.setURL("todos"); + const response = await TodoTestService.post(body); + todos.value.push(response.data); + } catch (error) { + console.error(error); + } finally { + endLoading(); + } + }; + + return { + statusCards, + todos, + fetchStatusCards, + fetchTodos, + addTodo, + isLoading, + }; +}); diff --git a/src/views/CommentsView.vue b/src/views/CommentsView.vue new file mode 100644 index 0000000..8113071 --- /dev/null +++ b/src/views/CommentsView.vue @@ -0,0 +1,159 @@ + +{{ $t("comments") }} + + + + {{ $t("Response All") }} + + + + + + + {{ $t('Confirm') }} + + + + + + + + + + + + + + + + + + + {{ $t("Pending") }} + + + + + {{ $t("Confirmed") }} + + + + + {{ $t("Rejected") }} + + + + + + + + + + + + + {{ $t("Show information") }} + + + + + + {{ commentModalTitleText(item) }} + + + + + + Comment Info: + + {{ $t("Body") }}: {{ item.body }} + + + {{ $t("Email") }}: {{ item.email }} + + + {{ $t("Status") }}: {{ item.status }} + + + + + Post Info: + + {{ $t("Title") }}: {{ relatedPost.title }} + + + {{ $t("Body") }}: {{ relatedPost.body }} + + + + + + + + + + + + + + + {{ $t("Confirm") }} + + + + + + + + + {{ $t("Reject") }} + + + + + + + + diff --git a/src/views/TodosView.vue b/src/views/TodosView.vue index 2124989..9559e29 100644 --- a/src/views/TodosView.vue +++ b/src/views/TodosView.vue @@ -45,7 +45,7 @@
+ {{ $t("Title") }}: {{ currentTodo?.title }} +
+ {{ $t("Status") }}: {{ currentTodo?.status }} +
{{ $t('Due') }}: {{ formatDate(currentTodo.dueDate) }}
+ {{ $t("Body") }}: {{ item.body }} +
+ {{ $t("Email") }}: {{ item.email }} +
+ {{ $t("Status") }}: {{ item.status }} +
+ {{ $t("Title") }}: {{ relatedPost.title }} +
+ {{ $t("Body") }}: {{ relatedPost.body }} +