diff --git a/.env.example b/.env.example index 4a653a4..8667c05 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,7 @@ LOG_LEVEL=info APP_KEY=TG5wX4cU9QL_Dch8q0Nbv37a3GssIPJc NODE_ENV=development SESSION_DRIVER=cookie +DB_CONNECTION=postgres DB_HOST=127.0.0.1 DB_PORT=5432 DB_USER=postgres diff --git a/.gitignore b/.gitignore index f49551f..86a6572 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,7 @@ yarn-error.log .DS_Store # railway -railway-prod-connection.json \ No newline at end of file +railway-prod-connection.json + +# db +*.sqlite \ No newline at end of file diff --git a/adonisrc.ts b/adonisrc.ts index 69b32ac..50dd9ad 100644 --- a/adonisrc.ts +++ b/adonisrc.ts @@ -42,6 +42,7 @@ export default defineConfig({ () => import('@adonisjs/auth/auth_provider'), () => import('@adonisjs/mail/mail_provider'), () => import('#providers/turbo/turbo_provider'), + () => import('#providers/data_table/datatable_builder_provider'), ], /* @@ -52,7 +53,11 @@ export default defineConfig({ | List of modules to import before starting the application. | */ - preloads: [() => import('#start/routes'), () => import('#start/kernel')], + preloads: [ + () => import('#start/routes'), + () => import('#start/kernel'), + () => import('#start/view'), + ], /* |-------------------------------------------------------------------------- diff --git a/app/controllers/employees_controller.ts b/app/controllers/employees_controller.ts new file mode 100644 index 0000000..97d2c07 --- /dev/null +++ b/app/controllers/employees_controller.ts @@ -0,0 +1,56 @@ +import Employee from '#models/employee' +import { employeeUpdateValidator } from '#validators/employee' +import type { HttpContext } from '@adonisjs/core/http' + +const tableConfig = { + baseUrl: '/employees', + pagination: { perPage: 10 }, + select: ['*'], + // mark filterable fields + // TODO - refactor to string[] + filterable: { + city: 'city', + name: 'name', + salary: 'salary', + }, + // mark sortable fields + sortable: { + salary: 'salary', + name: 'name', + city: 'city', + }, + // mark searchable fields + searchable: ['name', 'city', 'position'], +} + +export default class EmployeesController { + async index(ctx: HttpContext) { + const { turboFrame } = ctx + + const employees = await Employee.query().datatable(ctx.request.all(), tableConfig) + + // return response.send(employees) + + return turboFrame.render('pages/employees/index', { employees }) + } + + async update({ params, request, turboStream }: HttpContext) { + const columns = await request.validateUsing(employeeUpdateValidator) + + const employee = await Employee.findOrFail(params.id) + + const saved = await employee.merge(columns).save() + + return turboStream + .update('pages/employees/_table_row', { employee: saved }, `table-row-${params.id}`) + .render() + } + + async delete({ params, turboStream }: HttpContext) { + const employee = await Employee.findOrFail(params.id) + + await employee.delete() + + return turboStream.remove(`table-row-${params.id}`).render() + } +} diff --git a/app/models/employee.ts b/app/models/employee.ts new file mode 100644 index 0000000..23f5764 --- /dev/null +++ b/app/models/employee.ts @@ -0,0 +1,25 @@ +import { DateTime } from 'luxon' +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class Employee extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare name: string + + @column() + declare city: string + + @column() + declare position: string + + @column() + declare salary: number + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime +} diff --git a/app/validators/employee.ts b/app/validators/employee.ts new file mode 100644 index 0000000..d63ca2e --- /dev/null +++ b/app/validators/employee.ts @@ -0,0 +1,10 @@ +import vine from '@vinejs/vine' + +export const employeeUpdateValidator = vine.compile( + vine.object({ + name: vine.string().trim().minLength(1).maxLength(256), + city: vine.string().trim().minLength(1).maxLength(256), + position: vine.string().trim().minLength(1).maxLength(256), + salary: vine.number().withoutDecimals().min(1), + }) +) diff --git a/config/database.ts b/config/database.ts index a97bc71..9476f25 100644 --- a/config/database.ts +++ b/config/database.ts @@ -2,8 +2,14 @@ import env from '#start/env' import { defineConfig } from '@adonisjs/lucid' const dbConfig = defineConfig({ - connection: 'postgres', + connection: env.get('DB_CONNECTION') || 'postgres', connections: { + sqlite: { + client: 'better-sqlite3', + connection: { + filename: './db.sqlite', + }, + }, postgres: { client: 'pg', connection: { diff --git a/database/factories/employee_factory.ts b/database/factories/employee_factory.ts new file mode 100644 index 0000000..2f84001 --- /dev/null +++ b/database/factories/employee_factory.ts @@ -0,0 +1,13 @@ +import factory from '@adonisjs/lucid/factories' +import Employee from '#models/employee' + +export const EmployeeFactory = factory + .define(Employee, async ({ faker }) => { + return { + name: faker.person.fullName(), + city: faker.location.city(), + position: faker.person.jobTitle(), + salary: faker.number.int({ min: 2000, max: 15000 }), + } + }) + .build() diff --git a/database/migrations/1720127077572_create_employee_table.ts b/database/migrations/1720127077572_create_employee_table.ts new file mode 100644 index 0000000..06abbe8 --- /dev/null +++ b/database/migrations/1720127077572_create_employee_table.ts @@ -0,0 +1,23 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'employees' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + + table.string('name') + table.string('city') + table.string('position') + table.integer('salary').checkPositive() + + table.timestamp('created_at') + table.timestamp('updated_at') + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/database/seeders/employee_seeder.ts b/database/seeders/employee_seeder.ts new file mode 100644 index 0000000..0de0054 --- /dev/null +++ b/database/seeders/employee_seeder.ts @@ -0,0 +1,8 @@ +import { EmployeeFactory } from '#database/factories/employee_factory' +import { BaseSeeder } from '@adonisjs/lucid/seeders' + +export default class extends BaseSeeder { + async run() { + await EmployeeFactory.createMany(900) + } +} diff --git a/package-lock.json b/package-lock.json index b780e08..7ced0bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "@hotwired/turbo": "^8.0.4", "@popperjs/core": "^2.11.8", "@vinejs/vine": "^2.0.0", + "better-sqlite3": "^11.2.1", "bootstrap": "^5.3.3", "edge.js": "^6.0.2", "luxon": "^3.4.4", @@ -3074,6 +3075,16 @@ "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, + "node_modules/better-sqlite3": { + "version": "11.2.1", + "resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-11.2.1.tgz", + "integrity": "sha512-Xbt1d68wQnUuFIEVsbt6V+RG30zwgbtCGQ4QOcXVrOH0FE4eHk64FWZ9NUfRHS4/x1PXqwz/+KOrnXD7f0WieA==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "prebuild-install": "^7.1.1" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -3086,6 +3097,60 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -3426,6 +3491,11 @@ "node": ">= 6" } }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, "node_modules/ci-info": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", @@ -3996,6 +4066,14 @@ "node": ">=6" } }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -4094,6 +4172,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "engines": { + "node": ">=8" + } + }, "node_modules/diff": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", @@ -4292,7 +4378,6 @@ "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", - "dev": true, "dependencies": { "once": "^1.4.0" } @@ -4945,6 +5030,14 @@ "url": "https://github.com/sindresorhus/execa?sponsor=1" } }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, "node_modules/extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -5065,6 +5158,11 @@ "url": "https://github.com/sindresorhus/file-type?sponsor=1" } }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, "node_modules/fill-range": { "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", @@ -5197,6 +5295,11 @@ "js-yaml": "^3.13.1" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, "node_modules/fs-readdir-recursive": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/fs-readdir-recursive/-/fs-readdir-recursive-1.1.0.tgz", @@ -5356,6 +5459,11 @@ "resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz", "integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA==" }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -6057,6 +6165,11 @@ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, "node_modules/internal-slot": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", @@ -8144,7 +8257,6 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8164,6 +8276,11 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, "node_modules/mri": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", @@ -8202,6 +8319,11 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", + "integrity": "sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==" + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -8216,6 +8338,17 @@ "node": ">= 0.6" } }, + "node_modules/node-abi": { + "version": "3.67.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.67.0.tgz", + "integrity": "sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-html-parser": { "version": "6.1.13", "resolved": "https://registry.npmjs.org/node-html-parser/-/node-html-parser-6.1.13.tgz", @@ -8364,7 +8497,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "dependencies": { "wrappy": "1" } @@ -9026,6 +9158,31 @@ "node": ">=0.10.0" } }, + "node_modules/prebuild-install": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.2.tgz", + "integrity": "sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^1.0.1", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -9139,7 +9296,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", - "dev": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -9234,6 +9390,28 @@ "node": ">= 0.8" } }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/react-is": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", @@ -10044,7 +10222,6 @@ "version": "7.6.0", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz", "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==", - "dev": true, "dependencies": { "lru-cache": "^6.0.0" }, @@ -10059,7 +10236,6 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "dev": true, "dependencies": { "yallist": "^4.0.0" }, @@ -10256,6 +10432,49 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/slash": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", @@ -10684,6 +10903,45 @@ "node": ">=12.20" } }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/tarn": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/tarn/-/tarn-3.0.2.tgz", @@ -10910,6 +11168,17 @@ "node": ">=0.6.x" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -11640,8 +11909,7 @@ "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" }, "node_modules/xtend": { "version": "4.0.2", @@ -11654,8 +11922,7 @@ "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/yargs-parser": { "version": "21.1.1", diff --git a/package.json b/package.json index 7b844ba..3c0fed1 100644 --- a/package.json +++ b/package.json @@ -68,6 +68,7 @@ "@hotwired/turbo": "^8.0.4", "@popperjs/core": "^2.11.8", "@vinejs/vine": "^2.0.0", + "better-sqlite3": "^11.2.1", "bootstrap": "^5.3.3", "edge.js": "^6.0.2", "luxon": "^3.4.4", diff --git a/providers/data_table/data_table.ts b/providers/data_table/data_table.ts new file mode 100644 index 0000000..6ff0f3f --- /dev/null +++ b/providers/data_table/data_table.ts @@ -0,0 +1,101 @@ +import { LucidModel, ModelQueryBuilderContract } from '@adonisjs/lucid/types/model' +import { DatabaseQueryBuilderContract } from '@adonisjs/lucid/types/querybuilder' +import ExtendedPaginator from './extended_paginator.js' + +export type DatatableConfig = { + baseUrl: string + pagination: { perPage: number } + select: string[] + searchable: string[] + sortable: Record + filterable: Record +} + +type BuilderContracts = + | DatabaseQueryBuilderContract + | ModelQueryBuilderContract + +export default class Datatable { + private query: () => BuilderContracts + private config: DatatableConfig + + constructor(query: () => BuilderContracts, config: DatatableConfig) { + this.query = query + this.config = config + } + + async apply(queryString: Record) { + const query = this.query.call(this) + + this.applySelect(query, queryString) + this.applySorting(query, queryString) + this.applyFilter(query, queryString) + this.applySearch(query, queryString) + + const paginator = await this.applyPaginate(query, queryString) + const extended = new ExtendedPaginator( + this.config, + queryString, + paginator.total, + paginator.perPage, + paginator.currentPage, + ...paginator.all() + ) + + return extended + } + + private applySelect(query: BuilderContracts, queryString: Record) { + const { columns } = queryString + + if (columns) { + query.select(...columns) + } else { + query.select(...this.config.select) + } + } + + private async applyPaginate(query: BuilderContracts, queryString: Record) { + const { page, perPage } = queryString + const paginator = await query.paginate( + Number(page) || 1, + Number(perPage) || this.config.pagination.perPage + ) + + return paginator + } + + private applySearch(query: BuilderContracts, queryString: Record) { + const { search } = queryString + + if (search) { + for (let searchable of this.config.searchable) { + query.ifDialect( + 'better-sqlite3', + (q) => q.orWhereLike(searchable, `%${search}%`), + (q) => q.orWhereILike(searchable, `%${search}%`) + ) + } + } + } + + private applyFilter(query: BuilderContracts, queryString: Record) { + const { filter, filterValue } = queryString + + for (let filterable in this.config.filterable) { + if (filter === filterable) { + query.where(filter, filterValue) + } + } + } + + private applySorting(query: BuilderContracts, queryString: Record) { + const { sort, order } = queryString + + for (let sortable in this.config.sortable) { + if (sortable === sort) { + query.orderBy(sortable, (order as 'asc' | 'desc') || 'desc') + } + } + } +} diff --git a/providers/data_table/datatable_builder_provider.ts b/providers/data_table/datatable_builder_provider.ts new file mode 100644 index 0000000..f6893f2 --- /dev/null +++ b/providers/data_table/datatable_builder_provider.ts @@ -0,0 +1,72 @@ +import edge from 'edge.js' +import Datatable, { DatatableConfig } from '#providers/data_table/data_table' +import type { ApplicationService } from '@adonisjs/core/types' +import { DatabaseQueryBuilder } from '@adonisjs/lucid/database' +import { ModelQueryBuilder } from '@adonisjs/lucid/orm' +import { LucidModel } from '@adonisjs/lucid/types/model' +import ExtendedPaginator from './extended_paginator.js' + +/** + * Declare datatable on the contract namespaces + */ +declare module '@adonisjs/lucid/types/querybuilder' { + export interface ChainableContract { + datatable: ( + queryString: Record, + config: DatatableConfig + ) => Promise> + } +} + +declare module '@adonisjs/lucid/database' { + interface DatabaseQueryBuilder { + datatable: ( + queryString: Record, + config: DatatableConfig + ) => Promise> + } +} + +declare module '@adonisjs/lucid/orm' { + interface ModelQueryBuilder { + datatable: ( + queryString: Record, + config: DatatableConfig + ) => Promise> + } +} + +/** + * Apply the macros on boot + */ + +export default class DatatableBuilderProvider { + constructor(protected app: ApplicationService) {} + + /** + * The container bindings have booted + */ + async boot() { + edge.global('clamp', function (min: number, max: number, number: number) { + return Math.max(min, Math.min(number, max)) + }) + + DatabaseQueryBuilder.macro( + 'datatable', + function (queryString: Record, config: DatatableConfig) { + // @ts-ignore + const self = this as DatabaseQueryBuilder + return new Datatable(() => self, config).apply(queryString) + } + ) + + ModelQueryBuilder.macro( + 'datatable', + function (queryString: Record, config: DatatableConfig) { + // @ts-ignore + const self = this as ModelQueryBuilder + return new Datatable(() => self, config).apply(queryString) + } + ) + } +} diff --git a/providers/data_table/extended_paginator.ts b/providers/data_table/extended_paginator.ts new file mode 100644 index 0000000..75b90d3 --- /dev/null +++ b/providers/data_table/extended_paginator.ts @@ -0,0 +1,76 @@ +import { SimplePaginator } from '@adonisjs/lucid/database' +import { SimplePaginatorContract } from '@adonisjs/lucid/types/querybuilder' +import { DatatableConfig } from './data_table.js' +import { LucidModel } from '@adonisjs/lucid/types/model' + +interface ExtendedPaginatorContract extends SimplePaginatorContract { + clearSearch(q: Record): string + appendQueryString(qs: Record): string + getQueryString(k: string): string + toggleSortOrder(sort: string): string + getSortOrderFor(k: string): string | null +} + +export default class ExtendedPaginator + extends SimplePaginator + implements ExtendedPaginatorContract +{ + declare dtBaseUrl: string + declare model: LucidModel | null + declare config: DatatableConfig + + constructor( + config: DatatableConfig, + queryString: Record, + totalNumber: number, + perPage: number, + currentPage: number, + ...rows: Result[] + ) { + super(totalNumber, perPage, currentPage, ...rows) + this.dtBaseUrl = config.baseUrl + this.baseUrl(config.baseUrl) + this.queryString(queryString) + } + + constructQueryStringFrom(qs: Record) { + return Object.entries(qs) + .map(([key, val]) => `${key}=${val}`) + .join('&') + } + + clearSearch() { + // @ts-ignore + const qs = { ...this.qs } + delete qs.search // remove search + delete qs.page // reset page + const newQueryString = this.constructQueryStringFrom(qs) + return newQueryString.length ? `${this.dtBaseUrl}?${newQueryString}` : this.dtBaseUrl + } + + appendQueryString(appendedQs: Record) { + // @ts-ignore + const qs = { ...this.qs, ...appendedQs } + const newQueryString = this.constructQueryStringFrom(qs) + return `${this.dtBaseUrl}?${newQueryString}` + } + + getQueryString(key: string) { + // @ts-ignore + return this.qs[key] + } + + toggleSortOrder(sort: string) { + const order = this.getQueryString('order') === 'asc' ? 'desc' : 'asc' + const newQueryString = this.appendQueryString({ sort, order }) + + return `${newQueryString}` + } + + getSortOrderFor(sort: string) { + if (this.getQueryString('sort') === sort) { + return this.getQueryString('order') || null + } + return null + } +} diff --git a/providers/turbo/todo.txt b/providers/turbo/todo.txt index 49b9496..e9a9164 100644 --- a/providers/turbo/todo.txt +++ b/providers/turbo/todo.txt @@ -10,11 +10,11 @@ Tasks: to add one new message to the list dynamically later. This is at the essence of the HTML-over-the-wire approach" This means that the partials should not have any directives. We add them when we respond with a turbostream template [] Add a provider for TurboStream if we need config - for example asset versioning, paths to layouts -[x] Add a pattern matching functionality of some type to ctx: switch (request.format) case html -> .. case turbo -> +[-] Add a pattern matching functionality of some type to ctx: switch (request.format) case html -> .. case turbo -> - Using if statements, easier and less confusing [] Populate turbo data/state with flashMessages [x] Figure out a better way to render multiple templates for streams (turbo drive should only render one template btw) -[] Add support for all directives on TurboStream and TurboDrive +[x] Add support for all directives on TurboStream and TurboDrive [x] Align with rails api => Ruby: render turbo_stream: turbo_stream.append(:dom_id, partial: "some/template", locals: { message: message }) JS: turboStream.append("selector", "some/template", { state }) @@ -27,13 +27,14 @@ Tasks: Where does this approach break down where we actually need to rely on turbo's handling of csrf-tokens? - If using x-csrf-token token we need to make sure that the header is merged and the -tag with token is attached on every new turbo frame and turbo stream + [] Reload the entire view on E_BAD_CSRF_TOKEN + flash message ??? -[] Turbo Frames - [x] Render a minimal layout ( + tags) when a turbo frame request is detected (request.headers["Turbo-Frame"]) +[x] Turbo Frames + [-] Render a minimal layout ( + tags) when a turbo frame request is detected (request.headers["Turbo-Frame"]) https://github.com/hotwired/turbo-rails/blob/main/app/controllers/turbo/frames/frame_request.rb NOTE!!! Doesn't seem to actually merge the head on turbo frame requests + Discussion https://github.com/hotwired/turbo/issues/1237 [x] Sets etag header to bust the cache -[] Expose TurboFrame & TurboStream classes to DI system (ie construct in provider) - Note: Is this possible in adonis v6? Seems to have been used as a technique for libraries in Adonis v5 a lot +[x] Expose TurboFrame & TurboStream classes to DI system (ie construct in provider) [] Configure edge plugin to render frames and streams https://github.com/adonisjs/inertia/blob/main/src/plugins/edge/plugin.ts \ No newline at end of file diff --git a/resources/css/app.css b/resources/css/app.css index b51f323..b33fec3 100644 --- a/resources/css/app.css +++ b/resources/css/app.css @@ -1,4 +1,57 @@ +@import 'bootstrap.min.css'; .turbo-progress-bar { height: 4px; background-color: red; } + +thead th:hover { + background: #eee; +} + +#employee-dialog { + position: fixed; + width: 100%; + bottom: 0; + border: 0; +} + +@media screen and (min-width: 768px) { + #employee-dialog { + width: 50%; + bottom: unset; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + } +} + +/* Prevent scrolling while dialog is open */ +body:has(dialog[open]) { + overflow: hidden; +} + +@keyframes fade-in { + 0% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes fade-out { + 0% { + opacity: 1; + } + 100% { + opacity: 0; + } +} + +dialog[data-dialog-target='dialog'][open] { + animation: fade-in 200ms forwards; +} + +dialog[data-dialog-target='dialog'][closing] { + animation: fade-out 200ms forwards; +} diff --git a/resources/js/app.ts b/resources/js/app.ts index 0479f9e..02e4e34 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,4 +1,3 @@ -import '../css/bootstrap.min.css' import 'bootstrap' import '@hotwired/turbo' import { Application } from '@hotwired/stimulus' @@ -7,6 +6,8 @@ import NotificationController from './controllers/notification.js' import FormItController from './controllers/form_it.js' import ClearFormController from './controllers/clear_form.js' import ForceReloadController from './controllers/force_reload.js' +import DialogController from './controllers/dialog.js' +import ConfirmFormSubmitController from './controllers/confirm_form_submit.js' declare global { interface Window { @@ -20,3 +21,5 @@ window.Stimulus.register('onchangeform', OnChangeFormController) window.Stimulus.register('notification', NotificationController) window.Stimulus.register('clear-form', ClearFormController) window.Stimulus.register('force-reload', ForceReloadController) +window.Stimulus.register('dialog', DialogController) +window.Stimulus.register('confirm-form-submit', ConfirmFormSubmitController) diff --git a/resources/js/controllers/confirm_form_submit.ts b/resources/js/controllers/confirm_form_submit.ts new file mode 100644 index 0000000..d2d8bad --- /dev/null +++ b/resources/js/controllers/confirm_form_submit.ts @@ -0,0 +1,16 @@ +import { Controller } from '@hotwired/stimulus' + +export default class ConfirmFormSubmitController extends Controller { + declare readonly formTarget: HTMLFormElement + + static targets = ['form'] + + submit() { + const isConfirmed = confirm('Are you sure?') + + if (isConfirmed) { + console.log(this.element) + ;(this.element as HTMLFormElement).submit() + } + } +} diff --git a/resources/js/controllers/dialog.ts b/resources/js/controllers/dialog.ts new file mode 100644 index 0000000..60fee8e --- /dev/null +++ b/resources/js/controllers/dialog.ts @@ -0,0 +1,55 @@ +import { Controller } from '@hotwired/stimulus' + +export default class DialogController extends Controller { + declare readonly dialogTarget: HTMLDialogElement + declare readonly openValue: boolean + + static targets = ['dialog'] + static values = { + open: { + type: Boolean, + default: false, + }, + } + + initialize() { + this.forceClose = this.forceClose.bind(this) + } + + connect(): void { + if (this.openValue) { + this.open() + } + + document.addEventListener('turbo:before-render', this.forceClose) + } + + disconnect(): void { + document.removeEventListener('turbo:before-render', this.forceClose) + } + + open(): void { + this.dialogTarget.showModal() + } + + close(): void { + this.dialogTarget.setAttribute('closing', '') + + Promise.all(this.dialogTarget.getAnimations().map((animation) => animation.finished)).then( + () => { + this.dialogTarget.removeAttribute('closing') + this.dialogTarget.close() + } + ) + } + + backdropClose(event: Event): void { + if ((event.target as HTMLElement) === this.dialogTarget) { + this.close() + } + } + + forceClose(): void { + this.dialogTarget.close() + } +} diff --git a/resources/views/components/caret.edge b/resources/views/components/caret.edge new file mode 100644 index 0000000..6cb72bb --- /dev/null +++ b/resources/views/components/caret.edge @@ -0,0 +1,54 @@ +@let(direction = $props.get('direction')) + +@if(direction === "asc") + +
+ {{-- caret up fill --}} + + + + {{-- caret down --}} + + + +
+ +@elseif(direction === "desc") + +
+ {{-- caret up --}} + + + + {{-- caret down fill --}} + + + +
+ +@else + +
+ {{-- caret up --}} + + + + {{-- caret down --}} + + + +
+ +@end \ No newline at end of file diff --git a/resources/views/turbo_frame.edge b/resources/views/components/turbo_frame.edge similarity index 100% rename from resources/views/turbo_frame.edge rename to resources/views/components/turbo_frame.edge diff --git a/resources/views/layout/main.edge b/resources/views/layout/main.edge index e017416..344b423 100644 --- a/resources/views/layout/main.edge +++ b/resources/views/layout/main.edge @@ -3,7 +3,7 @@ - + Red Digital - {{ $props.get('title') }} @vite(['resources/css/app.css', 'resources/js/app.ts']) diff --git a/resources/views/pages/auth/register.edge b/resources/views/pages/auth/register.edge index 538eeaa..b5bd70e 100644 --- a/resources/views/pages/auth/register.edge +++ b/resources/views/pages/auth/register.edge @@ -5,6 +5,11 @@ title: "Register Account"
+ @error('E_VALIDATION_ERROR') + + @end
Register Account diff --git a/resources/views/pages/employees/_table_row.edge b/resources/views/pages/employees/_table_row.edge new file mode 100644 index 0000000..fed5e64 --- /dev/null +++ b/resources/views/pages/employees/_table_row.edge @@ -0,0 +1,30 @@ +{{ employee.name }} +{{ employee.position }} +{{ employee.city }} +€{{ employee.salary }}.00 + + + @turboFrame({ id: "dialog" }) +
+ {{ csrfField() }} +
+
Edit Employee
+ + + + + + +
+
+ @end +
+ + + + +
+ {{ csrfField() }} + +
+ diff --git a/resources/views/pages/employees/index.edge b/resources/views/pages/employees/index.edge new file mode 100644 index 0000000..fa40891 --- /dev/null +++ b/resources/views/pages/employees/index.edge @@ -0,0 +1,149 @@ +@let(nextPageUrl = employees.getNextPageUrl()) +@let(prevPageUrl = employees.getPreviousPageUrl()) +@let(perPageList = [10, 20, 30]) + +@component('layout/main', { +title: "Employees" +}) +
+ @turboFrame({ id: "employee-table", action: "advance" }) + {{-- Search form --}} +
+
+
+ +
+ @if(employees.getQueryString('search')) +
+ Clear +
+ @end +
+
+ + {{-- Per page form --}} +
+
+
+ @if(employees.getQueryString('order')) + + @end + @if(employees.getQueryString('sort')) + + @end + @if(employees.getQueryString('search')) + + @end + + +
+
+
+ + {{-- Table --}} +
+ + + + + + + + + + + @each(employee in employees) + + @include('pages/employees/_table_row.edge') + + @endeach + @if(!(employees.length > 0)) + + + + @end + +
+ + Name + @!caret({ fill: "primary", direction: employees.getSortOrderFor('name') }) + + Position + + City + @!caret({ fill: "primary",direction: employees.getSortOrderFor('city') }) + + + + Salary + @!caret({ fill: "primary",direction: employees.getSortOrderFor('salary') }) + +
No matching records found
+
+ + @if(employees.hasPages) +
+ @if (prevPageUrl) + + + + + Prev + + @else + + @end + + {{-- Pagination form --}} +
+ {{-- query strings saved when submitting the form --}} + @if(employees.getQueryString('order')) + + @end + @if(employees.getQueryString('sort')) + + @end + @if(employees.getQueryString('search')) + + @end + @if(employees.getQueryString('perPage')) + + @end +
+ Page + + of {{employees.lastPage}} +
+
+ + @if (nextPageUrl) + + Next + + + + + @else + + @end +
+ @end + @end +
+@end \ No newline at end of file diff --git a/resources/views/pages/home.edge b/resources/views/pages/home.edge index 44035a5..14be0c6 100644 --- a/resources/views/pages/home.edge +++ b/resources/views/pages/home.edge @@ -3,79 +3,8 @@ title: "Web & App Development" })

It Works!

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TypeColumn headingColumn headingColumn heading
ActiveColumn contentColumn contentColumn content
DefaultColumn contentColumn contentColumn content
PrimaryColumn contentColumn contentColumn content
SecondaryColumn contentColumn contentColumn content
SuccessColumn contentColumn contentColumn content
DangerColumn contentColumn contentColumn content
WarningColumn contentColumn contentColumn content
InfoColumn contentColumn contentColumn content
LightColumn contentColumn contentColumn content
DarkColumn contentColumn contentColumn content
-
+
@end \ No newline at end of file diff --git a/resources/views/pages/todos/_lazy.edge b/resources/views/pages/todos/_lazy.edge index 734f91e..d3be02f 100644 --- a/resources/views/pages/todos/_lazy.edge +++ b/resources/views/pages/todos/_lazy.edge @@ -1,3 +1,3 @@ -@component('turbo_frame', { id: "lazy" }) -
{{message}}
+@turboFrame({id: "lazy"}) +
{{message}}
@end \ No newline at end of file diff --git a/resources/views/pages/todos/_task.edge b/resources/views/pages/todos/_task.edge index 3614e26..af03103 100644 --- a/resources/views/pages/todos/_task.edge +++ b/resources/views/pages/todos/_task.edge @@ -1,4 +1,4 @@ -@component('turbo_frame', { id: `todo-${todo.id}` }) +@turboFrame({ id: `todo-${todo.id}` })
  • diff --git a/resources/views/pages/todos/index.edge b/resources/views/pages/todos/index.edge index 2fcd7ef..6ee1b08 100644 --- a/resources/views/pages/todos/index.edge +++ b/resources/views/pages/todos/index.edge @@ -3,7 +3,7 @@

    Task Manager

    - @component('turbo_frame', { id: "lazy", src: route("todos.lazy") }) + @turboFrame({ id: "lazy", src: route("todos.lazy") }) Loading @end diff --git a/start/env.ts b/start/env.ts index 78b0492..2a5b782 100644 --- a/start/env.ts +++ b/start/env.ts @@ -30,6 +30,7 @@ export default await Env.create(new URL('../', import.meta.url), { | Variables for configuring database connection |---------------------------------------------------------- */ + // DB_CONNECTION: Env.schema.string(), DB_HOST: Env.schema.string({ format: 'host' }), DB_PORT: Env.schema.number(), DB_USER: Env.schema.string(), @@ -42,5 +43,5 @@ export default await Env.create(new URL('../', import.meta.url), { |---------------------------------------------------------- */ SMTP_HOST: Env.schema.string(), - SMTP_PORT: Env.schema.string() + SMTP_PORT: Env.schema.string(), }) diff --git a/start/routes.ts b/start/routes.ts index dff50ca..3660bc7 100644 --- a/start/routes.ts +++ b/start/routes.ts @@ -9,6 +9,8 @@ import router from '@adonisjs/core/services/router' import { middleware } from './kernel.js' + +const EmployeesController = () => import('#controllers/employees_controller') const TodosController = () => import('#controllers/todos_controller') const ProfilesController = () => import('#controllers/profiles_controller') const PasswordResetController = () => import('#controllers/auth/password_reset_controller') @@ -136,3 +138,27 @@ router.post('/todos', [TodosController, 'save']).as('todos.save').use(middleware router.put('/todos/:id', [TodosController, 'update']).as('todos.update').use(middleware.auth()) router.delete('/todos/:id', [TodosController, 'delete']).as('todos.delete').use(middleware.auth()) router.get('/todos/lazyloaded', [TodosController, 'lazy']).as('todos.lazy').use(middleware.auth()) + +/* +|-------------------------------------------------------------------------- +| Employee datatable +|-------------------------------------------------------------------------- +| +| Test out some more advanced features for hotwire +| +*/ + +router + .get('/employees', [EmployeesController, 'index']) + .as('employees.index') + .use(middleware.auth()) + +router + .delete('/employees/:id', [EmployeesController, 'delete']) + .as('employees.delete') + .use(middleware.auth()) + +router + .put('/employees/edit/:id', [EmployeesController, 'update']) + .as('employees.update') + .use(middleware.auth()) diff --git a/start/view.ts b/start/view.ts new file mode 100644 index 0000000..626daec --- /dev/null +++ b/start/view.ts @@ -0,0 +1,3 @@ +import edge from 'edge.js' + +edge.global('clamp', (num: number, min: number, max: number) => Math.min(Math.max(num, min), max))