├── .adonisrc.json ├── .editorconfig ├── .env.example ├── .env.test ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .npmrc ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── settings.json ├── README.md ├── ace ├── ace-manifest.json ├── app ├── Controllers │ └── Http │ │ ├── AuthController.ts │ │ ├── DashboardController.ts │ │ ├── GardensController.ts │ │ ├── HomeController.ts │ │ └── SettingsController.ts ├── Exceptions │ └── Handler.ts ├── Middleware │ ├── Auth.ts │ ├── Csrf.ts │ ├── DetectUserLocale.ts │ ├── ErrorPages.ts │ └── SilentAuth.ts ├── Models │ ├── Garden.ts │ └── User.ts ├── Policies │ └── GardenPolicy.ts ├── Strategies │ └── CamelCaseNamingStrategy.ts └── Validators │ ├── GardenValidator.ts │ ├── SignInValidator.ts │ └── SignUpValidator.ts ├── commands └── index.ts ├── config ├── app.ts ├── auth.ts ├── bodyparser.ts ├── cors.ts ├── database.ts ├── drive.ts ├── hash.ts ├── i18n.ts ├── mail.ts ├── radonis.ts ├── redis.ts ├── session.ts ├── shield.ts └── static.ts ├── contracts ├── auth.ts ├── bouncer.ts ├── drive.ts ├── env.ts ├── events.ts ├── hash.ts ├── mail.ts ├── radonis.ts ├── redis.ts └── tests.ts ├── database ├── factories │ ├── GardenFactory.ts │ ├── UserFactory.ts │ └── index.ts ├── migrations │ ├── 1649328692013_users.ts │ └── 1651250696290_gardens.ts └── seeders │ └── Garden.ts ├── docker-compose.yml ├── env.ts ├── global.d.ts ├── package-lock.json ├── package.json ├── providers └── AppProvider.ts ├── public └── favicon.ico ├── resources ├── components │ ├── Auth │ │ ├── SignInForm.island.tsx │ │ └── SignUpForm.island.tsx │ ├── Button.island.tsx │ ├── Card.island.tsx │ ├── Checkbox.island.tsx │ ├── CsrfField.tsx │ ├── Fallback.tsx │ ├── Gardens │ │ ├── GardenForm.island.tsx │ │ └── GardensList.island.tsx │ ├── Grid.tsx │ ├── Header.island.tsx │ ├── IconCircle.tsx │ ├── Illustrations │ │ ├── ErrorIllustration.tsx │ │ ├── NoDataIllustration.tsx │ │ └── NotFoundIllustration.tsx │ ├── Input.island.tsx │ ├── Link.tsx │ ├── Logo.tsx │ ├── Modal.island.tsx │ ├── Search.island.tsx │ ├── Sidebar.tsx │ └── SidebarUserInfo.island.tsx ├── entry.client.ts ├── hooks │ ├── useAuthenticatedUser.ts │ ├── useCsrfToken.ts │ ├── useFormField.ts │ ├── useNavigation.ts │ └── useNavigationBuilder.ts ├── lang │ └── en │ │ ├── auth.json │ │ ├── dashboard.json │ │ ├── errors.json │ │ ├── gardens.json │ │ ├── home.json │ │ ├── navigation.json │ │ ├── settings.json │ │ ├── shared.json │ │ └── validator.json ├── layouts │ ├── Auth.tsx │ └── Base.tsx ├── utils │ └── string.ts └── views │ ├── Auth │ ├── SignIn.tsx │ ├── SignUp.tsx │ └── index.ts │ ├── Dashboard │ └── index.tsx │ ├── Errors │ └── InternalServerError.tsx │ ├── Gardens │ ├── Create.tsx │ ├── Edit.tsx │ ├── Show.tsx │ └── index.tsx │ ├── Home │ └── index.tsx │ └── Settings │ └── index.tsx ├── server.ts ├── start ├── bouncer.ts ├── kernel.ts └── routes.ts ├── test.ts ├── tests ├── bootstrap.ts └── functional │ └── .gitkeep └── tsconfig.json /.adonisrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript": true, 3 | "commands": [ 4 | "./commands", 5 | "@adonisjs/core/build/commands/index.js", 6 | "@adonisjs/repl/build/commands/index.js", 7 | "@adonisjs/lucid/build/commands/index.js", 8 | "@adonisjs/mail/build/commands/index.js", 9 | "@adonisjs/bouncer/build/commands/index.js", 10 | "@microeinhundert/radonis-server/build/commands/index.js" 11 | ], 12 | "exceptionHandlerNamespace": "App/Exceptions/Handler", 13 | "aliases": { 14 | "App": "app", 15 | "Config": "config", 16 | "Database": "database", 17 | "Contracts": "contracts", 18 | "Views": "resources/views", 19 | "Components": "resources/components", 20 | "Layouts": "resources/layouts", 21 | "resources": "resources" 22 | }, 23 | "preloads": [ 24 | "./start/routes", 25 | "./start/kernel", 26 | "./start/bouncer" 27 | ], 28 | "providers": [ 29 | "./providers/AppProvider", 30 | "@adonisjs/core", 31 | "@adonisjs/session", 32 | "@adonisjs/shield", 33 | "@adonisjs/lucid", 34 | "@adonisjs/auth", 35 | "@adonisjs/i18n", 36 | "@adonisjs/redis", 37 | "@adonisjs/mail", 38 | "@adonisjs/drive-s3", 39 | "@adonisjs/bouncer", 40 | "@japa/preset-adonis/TestsProvider", 41 | "@microeinhundert/radonis-server" 42 | ], 43 | "metaFiles": [ 44 | { 45 | "pattern": "public/**", 46 | "reloadServer": false 47 | }, 48 | { 49 | "pattern": "resources/lang/**/*.(json|yaml)", 50 | "reloadServer": true 51 | } 52 | ], 53 | "aceProviders": [ 54 | "@adonisjs/repl" 55 | ], 56 | "tests": { 57 | "suites": [ 58 | { 59 | "name": "functional", 60 | "files": [ 61 | "tests/functional/**/*.spec(.ts|.js)" 62 | ], 63 | "timeout": 60000 64 | } 65 | ] 66 | }, 67 | "testProviders": [ 68 | "@japa/preset-adonis/TestsProvider" 69 | ] 70 | } 71 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | [*] 2 | indent_style = space 3 | indent_size = 2 4 | end_of_line = lf 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | insert_final_newline = true 8 | 9 | [*.json] 10 | insert_final_newline = ignore 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | PORT=3333 2 | HOST=0.0.0.0 3 | NODE_ENV=development 4 | APP_KEY=NXNlJBabvGZr9hr2dGvK3cWTU7oPobFc 5 | DRIVE_DISK=local 6 | SESSION_DRIVER=cookie 7 | DB_CONNECTION=pg 8 | 9 | PG_HOST=localhost 10 | PG_PORT=5432 11 | PG_USER=lucid 12 | PG_PASSWORD=kdisjd8723 13 | PG_DB_NAME=lucid 14 | 15 | REDIS_CONNECTION=local 16 | REDIS_HOST=127.0.0.1 17 | REDIS_PORT=6379 18 | REDIS_PASSWORD= 19 | 20 | SES_ACCESS_KEY=dummyKey 21 | SES_ACCESS_SECRET=dummySecret 22 | SES_REGION=eu-central-1 23 | 24 | S3_KEY=dummyKey 25 | S3_SECRET=dummySecret 26 | S3_BUCKET=dummyBucket 27 | S3_REGION=dummyRegion 28 | S3_ENDPOINT=dummyEndpoint 29 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | NODE_ENV=test 2 | ASSETS_DRIVER=fake 3 | SESSION_DRIVER=memory 4 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | build 2 | tmp 3 | public/radonis 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "plugin:adonis/typescriptApp", 4 | "prettier", 5 | "plugin:react/recommended", 6 | "plugin:react/jsx-runtime", 7 | "plugin:react-hooks/recommended", 8 | "plugin:jsx-a11y/recommended" 9 | ], 10 | "plugins": ["prettier", "simple-import-sort"], 11 | "parserOptions": { 12 | "ecmaFeatures": { 13 | "jsx": true 14 | } 15 | }, 16 | "settings": { 17 | "react": { 18 | "version": "18" 19 | } 20 | }, 21 | "rules": { 22 | "react/jsx-sort-props": [ 23 | "error", 24 | { 25 | "callbacksLast": true, 26 | "shorthandLast": true, 27 | "reservedFirst": true 28 | } 29 | ], 30 | "react/prop-types": "off", 31 | "react/display-name": "off", 32 | "prettier/prettier": "error", 33 | "simple-import-sort/imports": "error", 34 | "simple-import-sort/exports": "error", 35 | "@typescript-eslint/consistent-type-imports": [ 36 | "error", 37 | { 38 | "prefer": "type-imports" 39 | } 40 | ] 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | coverage 4 | .DS_Store 5 | .env 6 | tmp 7 | public/radonis 8 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | loglevel=error 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build 2 | tmp 3 | public/radonis 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": true, 4 | "singleQuote": false, 5 | "useTabs": false, 6 | "quoteProps": "consistent", 7 | "bracketSpacing": true, 8 | "arrowParens": "always", 9 | "printWidth": 100 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } 7 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Radonis Example Application 2 | 3 | ## Getting Started 4 | 5 | Create a `.env` file in the project root. For default environment variables, refer to `.env.example`. 6 | 7 | Start the database locally: 8 | 9 | ```console 10 | docker compose up -d 11 | ``` 12 | 13 | Migrate the database for the first time: 14 | 15 | ```console 16 | node ace migration:fresh 17 | ``` 18 | 19 | Start the project for development: 20 | 21 | ```console 22 | npm run dev 23 | ``` 24 | 25 | Build the project for production: 26 | 27 | ```console 28 | npm run build 29 | ``` 30 | 31 | > Tip: Install the AdonisJS VSCode extension for a better development experience. 32 | -------------------------------------------------------------------------------- /ace: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Ace Commands 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is the entry point for running ace commands. 7 | | 8 | */ 9 | 10 | require('reflect-metadata') 11 | require('source-map-support').install({ handleUncaughtExceptions: false }) 12 | 13 | const { Ignitor } = require('@adonisjs/core/build/standalone') 14 | new Ignitor(__dirname) 15 | .ace() 16 | .handle(process.argv.slice(2)) 17 | -------------------------------------------------------------------------------- /app/Controllers/Http/AuthController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import User from "App/Models/User"; 3 | import SignInValidator from "App/Validators/SignInValidator"; 4 | import SignUpValidator from "App/Validators/SignUpValidator"; 5 | import { SignIn, SignUp } from "Views/Auth"; 6 | 7 | export default class AuthController { 8 | /* 9 | * signUpShow action 10 | */ 11 | public signUpShow({ radonis, i18n }: HttpContextContract) { 12 | return radonis.withHeadTitle(i18n.formatMessage("auth.signUp.title")).render(SignUp); 13 | } 14 | 15 | /* 16 | * signInShow action 17 | */ 18 | public signInShow({ radonis, i18n }: HttpContextContract) { 19 | return radonis.withHeadTitle(i18n.formatMessage("auth.signIn.title")).render(SignIn); 20 | } 21 | 22 | /* 23 | * signUp action 24 | */ 25 | public async signUp({ response, request, auth }: HttpContextContract) { 26 | const data = await request.validate(SignUpValidator); 27 | 28 | const user = await User.create(data); 29 | 30 | await auth.login(user); 31 | 32 | if (request.accepts(["html"])) { 33 | return response.redirect().toRoute("dashboard"); 34 | } 35 | 36 | return response.json(true); 37 | } 38 | 39 | /* 40 | * signIn action 41 | */ 42 | public async signIn({ response, request, auth, session, i18n }: HttpContextContract) { 43 | const { email, password, rememberMe } = await request.validate(SignInValidator); 44 | 45 | try { 46 | await auth.attempt(email, password, rememberMe); 47 | 48 | if (request.accepts(["html"])) { 49 | return response.redirect().toRoute("dashboard"); 50 | } 51 | 52 | return response.json(true); 53 | } catch { 54 | session.flash({ 55 | email, 56 | errors: { 57 | invalidEmailOrPassword: i18n.formatMessage("validator.invalidEmailOrPassword"), 58 | }, 59 | }); 60 | 61 | if (request.accepts(["html"])) { 62 | return response.redirect().back(); 63 | } 64 | 65 | return response.json(false); 66 | } 67 | } 68 | 69 | /* 70 | * signOut action 71 | */ 72 | public async signOut({ response, request, auth }: HttpContextContract) { 73 | await auth.logout(); 74 | 75 | if (request.accepts(["html"])) { 76 | return response.redirect().toRoute("home"); 77 | } 78 | 79 | return response.json(true); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /app/Controllers/Http/DashboardController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { Index } from "Views/Dashboard"; 3 | 4 | export default class DashboardController { 5 | /* 6 | * index action 7 | */ 8 | public index({ radonis, i18n }: HttpContextContract) { 9 | return radonis.withHeadTitle(i18n.formatMessage("dashboard.index.title")).render(Index); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/Controllers/Http/GardensController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import Garden from "App/Models/Garden"; 3 | import GardenValidator from "App/Validators/GardenValidator"; 4 | import { Create, Edit, Index, Show } from "Views/Gardens"; 5 | 6 | export default class GardensController { 7 | /* 8 | * index action 9 | */ 10 | public async index({ bouncer, radonis, auth, i18n }: HttpContextContract) { 11 | await bouncer.with("GardenPolicy").authorize("list"); 12 | 13 | const gardens = await Garden.query().where("user_id", auth.user!.id).select("*"); 14 | 15 | return radonis 16 | .withHeadTitle(i18n.formatMessage("gardens.index.title")) 17 | .render(Index, { gardens }); 18 | } 19 | 20 | /* 21 | * create action 22 | */ 23 | public async create({ bouncer, radonis, i18n }: HttpContextContract) { 24 | await bouncer.with("GardenPolicy").authorize("create"); 25 | 26 | return radonis.withHeadTitle(i18n.formatMessage("gardens.create.title")).render(Create); 27 | } 28 | 29 | /* 30 | * show action 31 | */ 32 | public async show({ bouncer, radonis, params, i18n }: HttpContextContract) { 33 | const garden = await Garden.findOrFail(params.id); 34 | 35 | await bouncer.with("GardenPolicy").authorize("view", garden); 36 | 37 | return radonis 38 | .withHeadTitle(i18n.formatMessage("gardens.show.title", { name: garden.name })) 39 | .render(Show, { garden }); 40 | } 41 | 42 | /* 43 | * edit action 44 | */ 45 | public async edit({ bouncer, radonis, params, i18n }: HttpContextContract) { 46 | const garden = await Garden.findOrFail(params.id); 47 | 48 | await bouncer.with("GardenPolicy").authorize("edit", garden); 49 | 50 | return radonis 51 | .withHeadTitle(i18n.formatMessage("gardens.edit.title", { name: garden.name })) 52 | .render(Edit, { garden }); 53 | } 54 | 55 | /* 56 | * store action 57 | */ 58 | public async store({ bouncer, request, response, auth }: HttpContextContract) { 59 | await bouncer.with("GardenPolicy").authorize("create"); 60 | 61 | const data = await request.validate(GardenValidator); 62 | 63 | const garden = await Garden.create({ 64 | ...data, 65 | userId: auth.user!.id, 66 | }); 67 | 68 | if (request.accepts(["html"])) { 69 | return response.redirect().toRoute("gardens.index"); 70 | } 71 | 72 | return response.json(garden); 73 | } 74 | 75 | /* 76 | * update action 77 | */ 78 | public async update({ bouncer, request, params, response }: HttpContextContract) { 79 | const garden = await Garden.findOrFail(params.id); 80 | 81 | await bouncer.with("GardenPolicy").authorize("edit", garden); 82 | 83 | const data = await request.validate(GardenValidator); 84 | 85 | const updatedGarden = await garden.merge(data).save(); 86 | 87 | if (request.accepts(["html"])) { 88 | return response.redirect().toRoute("gardens.index"); 89 | } 90 | 91 | return response.json(updatedGarden); 92 | } 93 | 94 | /* 95 | * destroy action 96 | */ 97 | public async destroy({ bouncer, request, params, response }: HttpContextContract) { 98 | const garden = await Garden.findOrFail(params.id); 99 | 100 | await bouncer.with("GardenPolicy").authorize("delete", garden); 101 | 102 | await garden.delete(); 103 | 104 | if (request.accepts(["html"])) { 105 | return response.redirect().back(); 106 | } 107 | 108 | return response.json(true); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /app/Controllers/Http/HomeController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { Index } from "Views/Home"; 3 | 4 | export default class HomeController { 5 | /* 6 | * index action 7 | */ 8 | public index({ radonis, i18n }: HttpContextContract) { 9 | return radonis.withHeadTitle(i18n.formatMessage("home.index.title")).render(Index); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/Controllers/Http/SettingsController.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { Index } from "Views/Settings"; 3 | 4 | export default class SettingsController { 5 | /* 6 | * index action 7 | */ 8 | public index({ radonis, i18n }: HttpContextContract) { 9 | return radonis.withHeadTitle(i18n.formatMessage("settings.index.title")).render(Index); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/Exceptions/Handler.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Http Exception Handler 4 | |-------------------------------------------------------------------------- 5 | | 6 | | AdonisJs will forward all exceptions occurred during an HTTP request to 7 | | the following class. You can learn more about exception handling by 8 | | reading docs. 9 | | 10 | | The exception handler extends a base `HttpExceptionHandler` which is not 11 | | mandatory, however it can do lot of heavy lifting to handle the errors 12 | | properly. 13 | | 14 | */ 15 | 16 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 17 | import HttpExceptionHandler from "@ioc:Adonis/Core/HttpExceptionHandler"; 18 | import Logger from "@ioc:Adonis/Core/Logger"; 19 | import { InternalServerError } from "Views/Errors/InternalServerError"; 20 | 21 | export default class ExceptionHandler extends HttpExceptionHandler { 22 | constructor() { 23 | super(Logger); 24 | } 25 | 26 | public async handle(error: any, ctx: HttpContextContract) { 27 | if (ctx.request.accepts(["html"]) && error.status === 500) { 28 | /** 29 | * Render error page 30 | */ 31 | return ctx.radonis.render(InternalServerError, { error }); 32 | } 33 | 34 | /** 35 | * Forward rest of the exceptions to the parent class 36 | */ 37 | return super.handle(error, ctx); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /app/Middleware/Auth.ts: -------------------------------------------------------------------------------- 1 | import { AuthenticationException } from "@adonisjs/auth/build/standalone"; 2 | import type { GuardsList } from "@ioc:Adonis/Addons/Auth"; 3 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 4 | 5 | export default class AuthMiddleware { 6 | protected redirectTo = "/signIn"; 7 | 8 | protected async authenticate(auth: HttpContextContract["auth"], guards: (keyof GuardsList)[]) { 9 | let guardLastAttempted: string | undefined; 10 | 11 | for (let guard of guards) { 12 | guardLastAttempted = guard; 13 | 14 | if (await auth.use(guard).check()) { 15 | auth.defaultGuard = guard; 16 | return true; 17 | } 18 | } 19 | 20 | throw new AuthenticationException( 21 | "Unauthorized access", 22 | "E_UNAUTHORIZED_ACCESS", 23 | guardLastAttempted, 24 | this.redirectTo 25 | ); 26 | } 27 | 28 | public async handle( 29 | { auth }: HttpContextContract, 30 | next: () => Promise, 31 | customGuards: (keyof GuardsList)[] 32 | ) { 33 | const guards = customGuards.length ? customGuards : [auth.name]; 34 | await this.authenticate(auth, guards); 35 | 36 | await next(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /app/Middleware/Csrf.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | 3 | export default class CsrfMiddleware { 4 | public async handle({ request, radonis }: HttpContextContract, next: () => Promise) { 5 | radonis.withHeadMeta({ "csrf-token": request.csrfToken }); 6 | radonis.withGlobals({ csrfToken: request.csrfToken }); 7 | 8 | await next(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/Middleware/DetectUserLocale.ts: -------------------------------------------------------------------------------- 1 | import I18n from "@ioc:Adonis/Addons/I18n"; 2 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 3 | 4 | export default class DetectUserLocaleMiddleware { 5 | protected getUserLanguage({ request }: HttpContextContract) { 6 | const availableLocales = I18n.supportedLocales(); 7 | 8 | return request.language(availableLocales) || request.input("lang") || I18n.defaultLocale; 9 | } 10 | 11 | public async handle(ctx: HttpContextContract, next: () => Promise) { 12 | const language = this.getUserLanguage(ctx); 13 | 14 | if (language) { 15 | ctx.i18n.switchLocale(language); 16 | } 17 | 18 | await next(); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /app/Middleware/ErrorPages.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { InternalServerError } from "Views/Errors/InternalServerError"; 3 | 4 | export default class ErrorPagesMiddleware { 5 | public async handle({ radonis }: HttpContextContract, next: () => Promise) { 6 | radonis.withErrorPages({ 7 | 500: InternalServerError, 8 | }); 9 | 10 | await next(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /app/Middleware/SilentAuth.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | 3 | export default class SilentAuthMiddleware { 4 | public async handle({ auth, radonis }: HttpContextContract, next: () => Promise) { 5 | await auth.check(); 6 | 7 | radonis.withGlobals({ authenticatedUser: auth.user }); 8 | 9 | await next(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /app/Models/Garden.ts: -------------------------------------------------------------------------------- 1 | import { BaseModel, BelongsTo, belongsTo, column } from "@ioc:Adonis/Lucid/Orm"; 2 | import User from "App/Models/User"; 3 | import CamelCaseNamingStrategy from "App/Strategies/CamelCaseNamingStrategy"; 4 | import { DateTime } from "luxon"; 5 | 6 | export default class Garden extends BaseModel { 7 | public static namingStrategy = new CamelCaseNamingStrategy(); 8 | 9 | /* 10 | * Columns 11 | */ 12 | @column({ isPrimary: true }) 13 | public id: number; 14 | 15 | @column() 16 | public name: string; 17 | 18 | @column() 19 | public zip: string; 20 | 21 | @column() 22 | public city: string; 23 | 24 | @column() 25 | public userId: number; 26 | 27 | @column.dateTime({ autoCreate: true }) 28 | public createdAt: DateTime; 29 | 30 | @column.dateTime({ autoCreate: true, autoUpdate: true }) 31 | public updatedAt: DateTime; 32 | 33 | /* 34 | * Relationships 35 | */ 36 | @belongsTo(() => User) 37 | public user: BelongsTo; 38 | } 39 | -------------------------------------------------------------------------------- /app/Models/User.ts: -------------------------------------------------------------------------------- 1 | import Hash from "@ioc:Adonis/Core/Hash"; 2 | import { BaseModel, beforeSave, column, HasMany, hasMany } from "@ioc:Adonis/Lucid/Orm"; 3 | import Garden from "App/Models/Garden"; 4 | import CamelCaseNamingStrategy from "App/Strategies/CamelCaseNamingStrategy"; 5 | import { DateTime } from "luxon"; 6 | 7 | export default class User extends BaseModel { 8 | public static namingStrategy = new CamelCaseNamingStrategy(); 9 | 10 | /* 11 | * Columns 12 | */ 13 | @column({ isPrimary: true }) 14 | public id: number; 15 | 16 | @column() 17 | public firstName: string; 18 | 19 | @column() 20 | public lastName: string; 21 | 22 | @column() 23 | public email: string; 24 | 25 | @column({ serializeAs: null }) 26 | public password: string; 27 | 28 | @column({ serializeAs: null }) 29 | public rememberMeToken?: string; 30 | 31 | @column.dateTime({ serializeAs: null, autoCreate: true }) 32 | public createdAt: DateTime; 33 | 34 | @column.dateTime({ serializeAs: null, autoCreate: true, autoUpdate: true }) 35 | public updatedAt: DateTime; 36 | 37 | /* 38 | * Relationships 39 | */ 40 | @hasMany(() => Garden) 41 | public gardens: HasMany; 42 | 43 | /* 44 | * Hooks 45 | */ 46 | @beforeSave() 47 | public static async hashPassword(user: User) { 48 | if (user.$dirty.password) { 49 | user.password = await Hash.make(user.password); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /app/Policies/GardenPolicy.ts: -------------------------------------------------------------------------------- 1 | import { BasePolicy } from "@ioc:Adonis/Addons/Bouncer"; 2 | import type Garden from "App/Models/Garden"; 3 | import type User from "App/Models/User"; 4 | 5 | export default class GardenPolicy extends BasePolicy { 6 | public async list(user: User) { 7 | return !!user.id; 8 | } 9 | 10 | public async view(user: User, garden: Garden) { 11 | return garden.userId === user.id; 12 | } 13 | 14 | public async create(user: User) { 15 | return !!user.id; 16 | } 17 | 18 | public async edit(user: User, garden: Garden) { 19 | return garden.userId === user.id; 20 | } 21 | 22 | public async delete(user: User, garden: Garden) { 23 | return garden.userId === user.id; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /app/Strategies/CamelCaseNamingStrategy.ts: -------------------------------------------------------------------------------- 1 | import { string } from "@ioc:Adonis/Core/Helpers"; 2 | import type { BaseModel } from "@ioc:Adonis/Lucid/Orm"; 3 | import { SnakeCaseNamingStrategy } from "@ioc:Adonis/Lucid/Orm"; 4 | 5 | export default class CamelCaseNamingStrategy extends SnakeCaseNamingStrategy { 6 | public serializedName(_model: typeof BaseModel, propertyName: string) { 7 | return string.camelCase(propertyName); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /app/Validators/GardenValidator.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { rules, schema } from "@ioc:Adonis/Core/Validator"; 3 | 4 | export default class GardenValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public refs = schema.refs({ 8 | id: this.ctx.params.id ?? 0, 9 | userId: this.ctx.auth.user?.id ?? 0, 10 | }); 11 | 12 | public schema = schema.create({ 13 | name: schema.string({ trim: true }, [ 14 | rules.unique({ 15 | table: "gardens", 16 | column: "name", 17 | whereNot: { 18 | id: this.refs.id, 19 | }, 20 | where: { 21 | user_id: this.refs.userId, 22 | }, 23 | }), 24 | ]), 25 | zip: schema.string({ trim: true }), 26 | city: schema.string({ trim: true }), 27 | }); 28 | 29 | public messages = {}; 30 | } 31 | -------------------------------------------------------------------------------- /app/Validators/SignInValidator.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { rules, schema } from "@ioc:Adonis/Core/Validator"; 3 | 4 | export default class SignInValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | email: schema.string({ trim: true }, [ 9 | rules.email(), 10 | rules.normalizeEmail({ 11 | allLowercase: true, 12 | gmailRemoveDots: false, 13 | gmailRemoveSubaddress: false, 14 | }), 15 | ]), 16 | password: schema.string({ trim: true }), 17 | rememberMe: schema.boolean.optional(), 18 | }); 19 | 20 | public messages = {}; 21 | } 22 | -------------------------------------------------------------------------------- /app/Validators/SignUpValidator.ts: -------------------------------------------------------------------------------- 1 | import type { HttpContextContract } from "@ioc:Adonis/Core/HttpContext"; 2 | import { rules, schema } from "@ioc:Adonis/Core/Validator"; 3 | 4 | export default class SignUpValidator { 5 | constructor(protected ctx: HttpContextContract) {} 6 | 7 | public schema = schema.create({ 8 | firstName: schema.string({ trim: true }), 9 | lastName: schema.string({ trim: true }), 10 | email: schema.string({ trim: true }, [ 11 | rules.email(), 12 | rules.normalizeEmail({ 13 | allLowercase: true, 14 | gmailRemoveDots: false, 15 | gmailRemoveSubaddress: false, 16 | }), 17 | rules.unique({ table: "users", column: "email" }), 18 | ]), 19 | password: schema.string({ trim: true }, [ 20 | rules.confirmed("passwordConfirmation"), 21 | rules.minLength(6), 22 | ]), 23 | }); 24 | 25 | public messages = {}; 26 | } 27 | -------------------------------------------------------------------------------- /commands/index.ts: -------------------------------------------------------------------------------- 1 | import { listDirectoryFiles } from "@adonisjs/core/build/standalone"; 2 | import Application from "@ioc:Adonis/Core/Application"; 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Exporting an array of commands 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Instead of manually exporting each file from this directory, we use the 10 | | helper `listDirectoryFiles` to recursively collect and export an array 11 | | of filenames. 12 | | 13 | | Couple of things to note: 14 | | 15 | | 1. The file path must be relative from the project root and not this directory. 16 | | 2. We must ignore this file to avoid getting into an infinite loop 17 | | 18 | */ 19 | export default listDirectoryFiles(__dirname, Application.appRoot, ["./commands/index"]); 20 | -------------------------------------------------------------------------------- /config/app.ts: -------------------------------------------------------------------------------- 1 | import Application from "@ioc:Adonis/Core/Application"; 2 | import type { AssetsManagerConfig } from "@ioc:Adonis/Core/AssetsManager"; 3 | import Env from "@ioc:Adonis/Core/Env"; 4 | import type { LoggerConfig } from "@ioc:Adonis/Core/Logger"; 5 | import type { ProfilerConfig } from "@ioc:Adonis/Core/Profiler"; 6 | import type { ServerConfig } from "@ioc:Adonis/Core/Server"; 7 | import type { ValidatorConfig } from "@ioc:Adonis/Core/Validator"; 8 | import proxyAddr from "proxy-addr"; 9 | 10 | /* 11 | |-------------------------------------------------------------------------- 12 | | Application secret key 13 | |-------------------------------------------------------------------------- 14 | | 15 | | The secret to encrypt and sign different values in your application. 16 | | Make sure to keep the `APP_KEY` as an environment variable and secure. 17 | | 18 | | Note: Changing the application key for an existing app will make all 19 | | the cookies invalid and also the existing encrypted data will not 20 | | be decrypted. 21 | | 22 | */ 23 | export const appKey: string = Env.get("APP_KEY"); 24 | 25 | /* 26 | |-------------------------------------------------------------------------- 27 | | Http server configuration 28 | |-------------------------------------------------------------------------- 29 | | 30 | | The configuration for the HTTP(s) server. Make sure to go through all 31 | | the config properties to make keep server secure. 32 | | 33 | */ 34 | export const http: ServerConfig = { 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Allow method spoofing 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Method spoofing enables defining custom HTTP methods using a query string 41 | | `_method`. This is usually required when you are making traditional 42 | | form requests and wants to use HTTP verbs like `PUT`, `DELETE` and 43 | | so on. 44 | | 45 | */ 46 | allowMethodSpoofing: true, 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Subdomain offset 51 | |-------------------------------------------------------------------------- 52 | */ 53 | subdomainOffset: 2, 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Request Ids 58 | |-------------------------------------------------------------------------- 59 | | 60 | | Setting this value to `true` will generate a unique request id for each 61 | | HTTP request and set it as `x-request-id` header. 62 | | 63 | */ 64 | generateRequestId: false, 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Trusting proxy servers 69 | |-------------------------------------------------------------------------- 70 | | 71 | | Define the proxy servers that AdonisJs must trust for reading `X-Forwarded` 72 | | headers. 73 | | 74 | */ 75 | trustProxy: proxyAddr.compile("loopback"), 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Generating Etag 80 | |-------------------------------------------------------------------------- 81 | | 82 | | Whether or not to generate an etag for every response. 83 | | 84 | */ 85 | etag: false, 86 | 87 | /* 88 | |-------------------------------------------------------------------------- 89 | | JSONP Callback 90 | |-------------------------------------------------------------------------- 91 | */ 92 | jsonpCallbackName: "callback", 93 | 94 | /* 95 | |-------------------------------------------------------------------------- 96 | | Cookie settings 97 | |-------------------------------------------------------------------------- 98 | */ 99 | cookie: { 100 | domain: "", 101 | path: "/", 102 | maxAge: "2h", 103 | httpOnly: true, 104 | secure: false, 105 | sameSite: false, 106 | }, 107 | }; 108 | 109 | /* 110 | |-------------------------------------------------------------------------- 111 | | Logger 112 | |-------------------------------------------------------------------------- 113 | */ 114 | export const logger: LoggerConfig = { 115 | /* 116 | |-------------------------------------------------------------------------- 117 | | Application name 118 | |-------------------------------------------------------------------------- 119 | | 120 | | The name of the application you want to add to the log. It is recommended 121 | | to always have app name in every log line. 122 | | 123 | | The `APP_NAME` environment variable is automatically set by AdonisJS by 124 | | reading the `name` property from the `package.json` file. 125 | | 126 | */ 127 | name: Env.get("APP_NAME"), 128 | 129 | /* 130 | |-------------------------------------------------------------------------- 131 | | Toggle logger 132 | |-------------------------------------------------------------------------- 133 | | 134 | | Enable or disable logger application wide 135 | | 136 | */ 137 | enabled: true, 138 | 139 | /* 140 | |-------------------------------------------------------------------------- 141 | | Logging level 142 | |-------------------------------------------------------------------------- 143 | | 144 | | The level from which you want the logger to flush logs. It is recommended 145 | | to make use of the environment variable, so that you can define log levels 146 | | at deployment level and not code level. 147 | | 148 | */ 149 | level: Env.get("LOG_LEVEL", "info"), 150 | 151 | /* 152 | |-------------------------------------------------------------------------- 153 | | Pretty print 154 | |-------------------------------------------------------------------------- 155 | | 156 | | It is highly advised NOT to use `prettyPrint` in production, since it 157 | | can have huge impact on performance. 158 | | 159 | */ 160 | prettyPrint: Env.get("NODE_ENV") === "development", 161 | }; 162 | 163 | /* 164 | |-------------------------------------------------------------------------- 165 | | Profiler 166 | |-------------------------------------------------------------------------- 167 | */ 168 | export const profiler: ProfilerConfig = { 169 | /* 170 | |-------------------------------------------------------------------------- 171 | | Toggle profiler 172 | |-------------------------------------------------------------------------- 173 | | 174 | | Enable or disable profiler 175 | | 176 | */ 177 | enabled: true, 178 | 179 | /* 180 | |-------------------------------------------------------------------------- 181 | | Blacklist actions/row labels 182 | |-------------------------------------------------------------------------- 183 | | 184 | | Define an array of actions or row labels that you want to disable from 185 | | getting profiled. 186 | | 187 | */ 188 | blacklist: [], 189 | 190 | /* 191 | |-------------------------------------------------------------------------- 192 | | Whitelist actions/row labels 193 | |-------------------------------------------------------------------------- 194 | | 195 | | Define an array of actions or row labels that you want to whitelist for 196 | | the profiler. When whitelist is defined, then `blacklist` is ignored. 197 | | 198 | */ 199 | whitelist: [], 200 | }; 201 | 202 | /* 203 | |-------------------------------------------------------------------------- 204 | | Validator 205 | |-------------------------------------------------------------------------- 206 | | 207 | | Configure the global configuration for the validator. Here's the reference 208 | | to the default config https://git.io/JT0WE 209 | | 210 | */ 211 | export const validator: ValidatorConfig = {}; 212 | 213 | /* 214 | |-------------------------------------------------------------------------- 215 | | Assets 216 | |-------------------------------------------------------------------------- 217 | | 218 | | Configure the asset manager you are using to compile the frontend assets 219 | | 220 | */ 221 | export const assets: AssetsManagerConfig = { 222 | /* 223 | |-------------------------------------------------------------------------- 224 | | Driver 225 | |-------------------------------------------------------------------------- 226 | | 227 | | Currently we only support webpack encore and may introduce more drivers 228 | | in the future 229 | | 230 | */ 231 | driver: "encore", 232 | 233 | /* 234 | |-------------------------------------------------------------------------- 235 | | Public path 236 | |-------------------------------------------------------------------------- 237 | | 238 | | Directory to search for the "manifest.json" and the "entrypoints.json" 239 | | files 240 | | 241 | */ 242 | publicPath: Application.publicPath("assets"), 243 | 244 | /* 245 | |-------------------------------------------------------------------------- 246 | | Script tag 247 | |-------------------------------------------------------------------------- 248 | | 249 | | Define attributes for the entryPointScripts tags 250 | | 251 | */ 252 | script: { 253 | attributes: { 254 | defer: true, 255 | }, 256 | }, 257 | 258 | /* 259 | |-------------------------------------------------------------------------- 260 | | Style tag 261 | |-------------------------------------------------------------------------- 262 | | 263 | | Define attributes for the entryPointStyles tags 264 | | 265 | */ 266 | style: { 267 | attributes: {}, 268 | }, 269 | }; 270 | -------------------------------------------------------------------------------- /config/auth.ts: -------------------------------------------------------------------------------- 1 | import type { AuthConfig } from "@ioc:Adonis/Addons/Auth"; 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Authentication Mapping 6 | |-------------------------------------------------------------------------- 7 | | 8 | | List of available authentication mapping. You must first define them 9 | | inside the `contracts/auth.ts` file before mentioning them here. 10 | | 11 | */ 12 | const authConfig: AuthConfig = { 13 | guard: "web", 14 | guards: { 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Web Guard 18 | |-------------------------------------------------------------------------- 19 | | 20 | | Web guard uses classic old school sessions for authenticating users. 21 | | If you are building a standard web application, it is recommended to 22 | | use web guard with session driver 23 | | 24 | */ 25 | web: { 26 | driver: "session", 27 | 28 | provider: { 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Driver 32 | |-------------------------------------------------------------------------- 33 | | 34 | | Name of the driver 35 | | 36 | */ 37 | driver: "lucid", 38 | 39 | /* 40 | |-------------------------------------------------------------------------- 41 | | Identifier key 42 | |-------------------------------------------------------------------------- 43 | | 44 | | The identifier key is the unique key on the model. In most cases specifying 45 | | the primary key is the right choice. 46 | | 47 | */ 48 | identifierKey: "id", 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Uids 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Uids are used to search a user against one of the mentioned columns. During 56 | | login, the auth module will search the user mentioned value against one 57 | | of the mentioned columns to find their user record. 58 | | 59 | */ 60 | uids: ["email"], 61 | 62 | /* 63 | |-------------------------------------------------------------------------- 64 | | Model 65 | |-------------------------------------------------------------------------- 66 | | 67 | | The model to use for fetching or finding users. The model is imported 68 | | lazily since the config files are read way earlier in the lifecycle 69 | | of booting the app and the models may not be in a usable state at 70 | | that time. 71 | | 72 | */ 73 | model: () => import("App/Models/User"), 74 | }, 75 | }, 76 | }, 77 | }; 78 | 79 | export default authConfig; 80 | -------------------------------------------------------------------------------- /config/bodyparser.ts: -------------------------------------------------------------------------------- 1 | import type { BodyParserConfig } from "@ioc:Adonis/Core/BodyParser"; 2 | 3 | const bodyParserConfig: BodyParserConfig = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | White listed methods 7 | |-------------------------------------------------------------------------- 8 | | 9 | | HTTP methods for which body parsing must be performed. It is a good practice 10 | | to avoid body parsing for `GET` requests. 11 | | 12 | */ 13 | whitelistedMethods: ["POST", "PUT", "PATCH", "DELETE"], 14 | 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | JSON parser settings 18 | |-------------------------------------------------------------------------- 19 | | 20 | | The settings for the JSON parser. The types defines the request content 21 | | types which gets processed by the JSON parser. 22 | | 23 | */ 24 | json: { 25 | encoding: "utf-8", 26 | limit: "1mb", 27 | strict: true, 28 | types: [ 29 | "application/json", 30 | "application/json-patch+json", 31 | "application/vnd.api+json", 32 | "application/csp-report", 33 | ], 34 | }, 35 | 36 | /* 37 | |-------------------------------------------------------------------------- 38 | | Form parser settings 39 | |-------------------------------------------------------------------------- 40 | | 41 | | The settings for the `application/x-www-form-urlencoded` parser. The types 42 | | defines the request content types which gets processed by the form parser. 43 | | 44 | */ 45 | form: { 46 | encoding: "utf-8", 47 | limit: "1mb", 48 | queryString: {}, 49 | 50 | /* 51 | |-------------------------------------------------------------------------- 52 | | Convert empty strings to null 53 | |-------------------------------------------------------------------------- 54 | | 55 | | Convert empty form fields to null. HTML forms results in field string 56 | | value when the field is left blank. This option normalizes all the blank 57 | | field values to "null" 58 | | 59 | */ 60 | convertEmptyStringsToNull: true, 61 | 62 | types: ["application/x-www-form-urlencoded"], 63 | }, 64 | 65 | /* 66 | |-------------------------------------------------------------------------- 67 | | Raw body parser settings 68 | |-------------------------------------------------------------------------- 69 | | 70 | | Raw body just reads the request body stream as a plain text, which you 71 | | can process by hand. This must be used when request body type is not 72 | | supported by the body parser. 73 | | 74 | */ 75 | raw: { 76 | encoding: "utf-8", 77 | limit: "1mb", 78 | queryString: {}, 79 | types: ["text/*"], 80 | }, 81 | 82 | /* 83 | |-------------------------------------------------------------------------- 84 | | Multipart parser settings 85 | |-------------------------------------------------------------------------- 86 | | 87 | | The settings for the `multipart/form-data` parser. The types defines the 88 | | request content types which gets processed by the form parser. 89 | | 90 | */ 91 | multipart: { 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Auto process 95 | |-------------------------------------------------------------------------- 96 | | 97 | | The auto process option will process uploaded files and writes them to 98 | | the `tmp` folder. You can turn it off and then manually use the stream 99 | | to pipe stream to a different destination. 100 | | 101 | | It is recommended to keep `autoProcess=true`. Unless you are processing bigger 102 | | file sizes. 103 | | 104 | */ 105 | autoProcess: true, 106 | 107 | /* 108 | |-------------------------------------------------------------------------- 109 | | Files to be processed manually 110 | |-------------------------------------------------------------------------- 111 | | 112 | | You can turn off `autoProcess` for certain routes by defining 113 | | routes inside the following array. 114 | | 115 | | NOTE: Make sure the route pattern starts with a leading slash. 116 | | 117 | | Correct 118 | | ```js 119 | | /projects/:id/file 120 | | ``` 121 | | 122 | | Incorrect 123 | | ```js 124 | | projects/:id/file 125 | | ``` 126 | */ 127 | processManually: [], 128 | 129 | /* 130 | |-------------------------------------------------------------------------- 131 | | Temporary file name 132 | |-------------------------------------------------------------------------- 133 | | 134 | | When auto processing is on. We will use this method to compute the temporary 135 | | file name. AdonisJs will compute a unique `tmpPath` for you automatically, 136 | | However, you can also define your own custom method. 137 | | 138 | */ 139 | // tmpFileName () { 140 | // }, 141 | 142 | /* 143 | |-------------------------------------------------------------------------- 144 | | Encoding 145 | |-------------------------------------------------------------------------- 146 | | 147 | | Request body encoding 148 | | 149 | */ 150 | encoding: "utf-8", 151 | 152 | /* 153 | |-------------------------------------------------------------------------- 154 | | Convert empty strings to null 155 | |-------------------------------------------------------------------------- 156 | | 157 | | Convert empty form fields to null. HTML forms results in field string 158 | | value when the field is left blank. This option normalizes all the blank 159 | | field values to "null" 160 | | 161 | */ 162 | convertEmptyStringsToNull: true, 163 | 164 | /* 165 | |-------------------------------------------------------------------------- 166 | | Max Fields 167 | |-------------------------------------------------------------------------- 168 | | 169 | | The maximum number of fields allowed in the request body. The field includes 170 | | text inputs and files both. 171 | | 172 | */ 173 | maxFields: 1000, 174 | 175 | /* 176 | |-------------------------------------------------------------------------- 177 | | Request body limit 178 | |-------------------------------------------------------------------------- 179 | | 180 | | The total limit to the multipart body. This includes all request files 181 | | and fields data. 182 | | 183 | */ 184 | limit: "20mb", 185 | 186 | /* 187 | |-------------------------------------------------------------------------- 188 | | Types 189 | |-------------------------------------------------------------------------- 190 | | 191 | | The types that will be considered and parsed as multipart body. 192 | | 193 | */ 194 | types: ["multipart/form-data"], 195 | }, 196 | }; 197 | 198 | export default bodyParserConfig; 199 | -------------------------------------------------------------------------------- /config/cors.ts: -------------------------------------------------------------------------------- 1 | import type { CorsConfig } from "@ioc:Adonis/Core/Cors"; 2 | 3 | const corsConfig: CorsConfig = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Enabled 7 | |-------------------------------------------------------------------------- 8 | | 9 | | A boolean to enable or disable CORS integration from your AdonisJs 10 | | application. 11 | | 12 | | Setting the value to `true` will enable the CORS for all HTTP request. However, 13 | | you can define a function to enable/disable it on per request basis as well. 14 | | 15 | */ 16 | enabled: false, 17 | 18 | // You can also use a function that return true or false. 19 | // enabled: (request) => request.url().startsWith('/api') 20 | 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | Origin 24 | |-------------------------------------------------------------------------- 25 | | 26 | | Set a list of origins to be allowed for `Access-Control-Allow-Origin`. 27 | | The value can be one of the following: 28 | | 29 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Origin 30 | | 31 | | Boolean (true) - Allow current request origin. 32 | | Boolean (false) - Disallow all. 33 | | String - Comma separated list of allowed origins. 34 | | Array - An array of allowed origins. 35 | | String (*) - A wildcard (*) to allow all request origins. 36 | | Function - Receives the current origin string and should return 37 | | one of the above values. 38 | | 39 | */ 40 | origin: true, 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Methods 45 | |-------------------------------------------------------------------------- 46 | | 47 | | An array of allowed HTTP methods for CORS. The `Access-Control-Request-Method` 48 | | is checked against the following list. 49 | | 50 | | Following is the list of default methods. Feel free to add more. 51 | */ 52 | methods: ["GET", "HEAD", "POST", "PUT", "DELETE"], 53 | 54 | /* 55 | |-------------------------------------------------------------------------- 56 | | Headers 57 | |-------------------------------------------------------------------------- 58 | | 59 | | List of headers to be allowed for `Access-Control-Allow-Headers` header. 60 | | The value can be one of the following: 61 | | 62 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Request-Headers 63 | | 64 | | Boolean(true) - Allow all headers mentioned in `Access-Control-Request-Headers`. 65 | | Boolean(false) - Disallow all headers. 66 | | String - Comma separated list of allowed headers. 67 | | Array - An array of allowed headers. 68 | | Function - Receives the current header and should return one of the above values. 69 | | 70 | */ 71 | headers: true, 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Expose Headers 76 | |-------------------------------------------------------------------------- 77 | | 78 | | A list of headers to be exposed by setting `Access-Control-Expose-Headers`. 79 | | header. By default following 6 simple response headers are exposed. 80 | | 81 | | Cache-Control 82 | | Content-Language 83 | | Content-Type 84 | | Expires 85 | | Last-Modified 86 | | Pragma 87 | | 88 | | In order to add more headers, simply define them inside the following array. 89 | | 90 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Expose-Headers 91 | | 92 | */ 93 | exposeHeaders: [ 94 | "cache-control", 95 | "content-language", 96 | "content-type", 97 | "expires", 98 | "last-modified", 99 | "pragma", 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Credentials 105 | |-------------------------------------------------------------------------- 106 | | 107 | | Toggle `Access-Control-Allow-Credentials` header. If value is set to `true`, 108 | | then header will be set, otherwise not. 109 | | 110 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Allow-Credentials 111 | | 112 | */ 113 | credentials: true, 114 | 115 | /* 116 | |-------------------------------------------------------------------------- 117 | | MaxAge 118 | |-------------------------------------------------------------------------- 119 | | 120 | | Define `Access-Control-Max-Age` header in seconds. 121 | | https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Access-Control-Max-Age 122 | | 123 | */ 124 | maxAge: 90, 125 | }; 126 | 127 | export default corsConfig; 128 | -------------------------------------------------------------------------------- /config/database.ts: -------------------------------------------------------------------------------- 1 | import Env from "@ioc:Adonis/Core/Env"; 2 | import type { DatabaseConfig } from "@ioc:Adonis/Lucid/Database"; 3 | 4 | const databaseConfig: DatabaseConfig = { 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Connection 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The primary connection for making database queries across the application 11 | | You can use any key from the `connections` object defined in this same 12 | | file. 13 | | 14 | */ 15 | connection: Env.get("DB_CONNECTION"), 16 | 17 | connections: { 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | PostgreSQL config 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Configuration for PostgreSQL database. Make sure to install the driver 24 | | from npm when using this connection 25 | | 26 | | npm i pg 27 | | 28 | */ 29 | pg: { 30 | client: "pg", 31 | connection: { 32 | host: Env.get("PG_HOST"), 33 | port: Env.get("PG_PORT"), 34 | user: Env.get("PG_USER"), 35 | password: Env.get("PG_PASSWORD", ""), 36 | database: Env.get("PG_DB_NAME"), 37 | }, 38 | migrations: { 39 | naturalSort: true, 40 | }, 41 | healthCheck: false, 42 | debug: false, 43 | }, 44 | }, 45 | }; 46 | 47 | export default databaseConfig; 48 | -------------------------------------------------------------------------------- /config/drive.ts: -------------------------------------------------------------------------------- 1 | import Application from "@ioc:Adonis/Core/Application"; 2 | import type { DriveConfig } from "@ioc:Adonis/Core/Drive"; 3 | import Env from "@ioc:Adonis/Core/Env"; 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Drive Config 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The `DriveConfig` relies on the `DisksList` interface which is 11 | | defined inside the `contracts` directory. 12 | | 13 | */ 14 | const driveConfig: DriveConfig = { 15 | /* 16 | |-------------------------------------------------------------------------- 17 | | Default disk 18 | |-------------------------------------------------------------------------- 19 | | 20 | | The default disk to use for managing file uploads. The value is driven by 21 | | the `DRIVE_DISK` environment variable. 22 | | 23 | */ 24 | disk: Env.get("DRIVE_DISK"), 25 | 26 | disks: { 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Local 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Uses the local file system to manage files. Make sure to turn off serving 33 | | files when not using this disk. 34 | | 35 | */ 36 | local: { 37 | driver: "local", 38 | visibility: "public", 39 | 40 | /* 41 | |-------------------------------------------------------------------------- 42 | | Storage root - Local driver only 43 | |-------------------------------------------------------------------------- 44 | | 45 | | Define an absolute path to the storage directory from where to read the 46 | | files. 47 | | 48 | */ 49 | root: Application.tmpPath("uploads"), 50 | 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Serve files - Local driver only 54 | |-------------------------------------------------------------------------- 55 | | 56 | | When this is set to true, AdonisJS will configure a files server to serve 57 | | files from the disk root. This is done to mimic the behavior of cloud 58 | | storage services that has inbuilt capabilities to serve files. 59 | | 60 | */ 61 | serveFiles: true, 62 | 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Base path - Local driver only 66 | |-------------------------------------------------------------------------- 67 | | 68 | | Base path is always required when "serveFiles = true". Also make sure 69 | | the `basePath` is unique across all the disks using "local" driver and 70 | | you are not registering routes with this prefix. 71 | | 72 | */ 73 | basePath: "/uploads", 74 | }, 75 | 76 | /* 77 | |-------------------------------------------------------------------------- 78 | | S3 Driver 79 | |-------------------------------------------------------------------------- 80 | | 81 | | Uses the S3 cloud storage to manage files. 82 | | 83 | */ 84 | s3: { 85 | driver: "s3", 86 | visibility: "public", 87 | forcePathStyle: true, 88 | key: Env.get("S3_KEY"), 89 | secret: Env.get("S3_SECRET"), 90 | region: Env.get("S3_REGION"), 91 | bucket: Env.get("S3_BUCKET"), 92 | endpoint: Env.get("S3_ENDPOINT"), 93 | }, 94 | }, 95 | }; 96 | 97 | export default driveConfig; 98 | -------------------------------------------------------------------------------- /config/hash.ts: -------------------------------------------------------------------------------- 1 | import Env from "@ioc:Adonis/Core/Env"; 2 | import type { HashConfig } from "@ioc:Adonis/Core/Hash"; 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Hash Config 7 | |-------------------------------------------------------------------------- 8 | | 9 | | The `HashConfig` relies on the `HashList` interface which is 10 | | defined inside `contracts` directory. 11 | | 12 | */ 13 | const hashConfig: HashConfig = { 14 | /* 15 | |-------------------------------------------------------------------------- 16 | | Default hasher 17 | |-------------------------------------------------------------------------- 18 | | 19 | | By default we make use of the argon hasher to hash values. However, feel 20 | | free to change the default value 21 | | 22 | */ 23 | default: Env.get("HASH_DRIVER", "argon"), 24 | 25 | list: { 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Argon 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Argon mapping uses the `argon2` driver to hash values. 32 | | 33 | | Make sure you install the underlying dependency for this driver to work. 34 | | https://www.npmjs.com/package/phc-argon2. 35 | | 36 | | npm install phc-argon2 37 | | 38 | */ 39 | argon: { 40 | driver: "argon2", 41 | variant: "id", 42 | iterations: 3, 43 | memory: 4096, 44 | parallelism: 1, 45 | saltSize: 16, 46 | }, 47 | 48 | /* 49 | |-------------------------------------------------------------------------- 50 | | Bcrypt 51 | |-------------------------------------------------------------------------- 52 | | 53 | | Bcrypt mapping uses the `bcrypt` driver to hash values. 54 | | 55 | | Make sure you install the underlying dependency for this driver to work. 56 | | https://www.npmjs.com/package/phc-bcrypt. 57 | | 58 | | npm install phc-bcrypt 59 | | 60 | */ 61 | bcrypt: { 62 | driver: "bcrypt", 63 | rounds: 10, 64 | }, 65 | }, 66 | }; 67 | 68 | export default hashConfig; 69 | -------------------------------------------------------------------------------- /config/i18n.ts: -------------------------------------------------------------------------------- 1 | import type { I18nConfig } from "@ioc:Adonis/Addons/I18n"; 2 | import Application from "@ioc:Adonis/Core/Application"; 3 | 4 | const i18nConfig: I18nConfig = { 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Translations format 8 | |-------------------------------------------------------------------------- 9 | | 10 | | The format in which the translation are written. By default only the 11 | | ICU message syntax is supported. However, you can register custom 12 | | formatters too and please reference the documentation for that. 13 | | 14 | */ 15 | translationsFormat: "icu", 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Default locale 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The default locale represents the language for which all the translations 23 | | are always available. 24 | | 25 | | Having a default locale allows you to incrementally add translations for 26 | | other languages. If a specific language does not have a translation, 27 | | then the default locale translation will be used. 28 | | 29 | | Also, we switch to default locale for HTTP requests where the user language 30 | | is not supported by the your app 31 | | 32 | */ 33 | defaultLocale: "en", 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Supported locales 38 | |-------------------------------------------------------------------------- 39 | | 40 | | Optionally define an array of locales that your application supports. If 41 | | not defined, we will derive this value from the translations stored 42 | | inside the `resources/lang` directory. 43 | | 44 | */ 45 | // supportedLocales: [], 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Fallback locales 50 | |-------------------------------------------------------------------------- 51 | | 52 | | Here you can configure per language fallbacks. For example, you can set 53 | | "es" as the fallback locale for the Catalan language. 54 | | 55 | | If not configured, all languages will fallback to the defaultLocale 56 | | 57 | */ 58 | // fallbackLocales: {}, 59 | 60 | /* 61 | |-------------------------------------------------------------------------- 62 | | Provide validator messages 63 | |-------------------------------------------------------------------------- 64 | | 65 | | Set the following option to "true" if you want to use "i18n" for defining 66 | | the validation messages. 67 | | 68 | | The validation messages will be loaded from the "validator.shared" prefix. 69 | | 70 | */ 71 | provideValidatorMessages: true, 72 | 73 | /* 74 | |-------------------------------------------------------------------------- 75 | | Loaders 76 | |-------------------------------------------------------------------------- 77 | | 78 | | Loaders from which to load the translations. You can configure multiple 79 | | loaders as well and AdonisJS will merge the translations from all the 80 | | loaders to have a unified collection of messages. 81 | | 82 | | By default, only the "fs" loader is supported. However, you can add custom 83 | | loaders too and please reference the documentation for that. 84 | | 85 | */ 86 | loaders: { 87 | fs: { 88 | enabled: true, 89 | location: Application.resourcesPath("lang"), 90 | }, 91 | }, 92 | }; 93 | 94 | export default i18nConfig; 95 | -------------------------------------------------------------------------------- /config/mail.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Config source: https://git.io/JvgAf 3 | * 4 | * Feel free to let us know via PR, if you find something broken in this contract 5 | * file. 6 | */ 7 | 8 | import type { MailConfig } from "@ioc:Adonis/Addons/Mail"; 9 | import Env from "@ioc:Adonis/Core/Env"; 10 | 11 | const mailConfig: MailConfig = { 12 | /* 13 | |-------------------------------------------------------------------------- 14 | | Default mailer 15 | |-------------------------------------------------------------------------- 16 | | 17 | | The following mailer will be used to send emails, when you don't specify 18 | | a mailer 19 | | 20 | */ 21 | mailer: "ses", 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Mailers 26 | |-------------------------------------------------------------------------- 27 | | 28 | | You can define or more mailers to send emails from your application. A 29 | | single `driver` can be used to define multiple mailers with different 30 | | config. 31 | | 32 | | For example: Postmark driver can be used to have different mailers for 33 | | sending transactional and promotional emails 34 | | 35 | */ 36 | mailers: { 37 | /* 38 | |-------------------------------------------------------------------------- 39 | | SES 40 | |-------------------------------------------------------------------------- 41 | | 42 | | Uses Amazon SES for sending emails. You will have to install the aws-sdk 43 | | when using this driver. 44 | | 45 | | ``` 46 | | npm i aws-sdk 47 | | ``` 48 | | 49 | */ 50 | ses: { 51 | driver: "ses", 52 | apiVersion: "2010-12-01", 53 | key: Env.get("SES_ACCESS_KEY"), 54 | secret: Env.get("SES_ACCESS_SECRET"), 55 | region: Env.get("SES_REGION"), 56 | sslEnabled: true, 57 | sendingRate: 10, 58 | maxConnections: 5, 59 | }, 60 | }, 61 | }; 62 | 63 | export default mailConfig; 64 | -------------------------------------------------------------------------------- /config/radonis.ts: -------------------------------------------------------------------------------- 1 | import Application from "@ioc:Adonis/Core/Application"; 2 | import type { RadonisConfig } from "@ioc:Microeinhundert/Radonis"; 3 | import { unocssPlugin } from "@microeinhundert/radonis-unocss"; 4 | 5 | const radonisConfig: RadonisConfig = { 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Plugins 9 | |-------------------------------------------------------------------------- 10 | | 11 | | The registered server plugins. Client plugins are registered 12 | | separately inside the client entry file. 13 | | 14 | */ 15 | plugins: [unocssPlugin()], 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Head 20 | |-------------------------------------------------------------------------- 21 | | 22 | | Configuration of the page . 23 | | 24 | */ 25 | head: { 26 | /* 27 | |-------------------------------------------------------------------------- 28 | | Title 29 | |-------------------------------------------------------------------------- 30 | | 31 | | Defaults and general configuration of the page . 32 | | 33 | */ 34 | title: { 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Default 38 | |-------------------------------------------------------------------------- 39 | | 40 | | The default title value. 41 | | Views without a title set will fall back to this value. 42 | | 43 | */ 44 | default: Application.appName, 45 | 46 | /* 47 | |-------------------------------------------------------------------------- 48 | | Prefix 49 | |-------------------------------------------------------------------------- 50 | | 51 | | A string to prefix the title with. 52 | | 53 | */ 54 | prefix: "", 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Suffix 59 | |-------------------------------------------------------------------------- 60 | | 61 | | A string to suffix the title with. 62 | | 63 | */ 64 | suffix: "Radonis Example Application", 65 | 66 | /* 67 | |-------------------------------------------------------------------------- 68 | | Separator 69 | |-------------------------------------------------------------------------- 70 | | 71 | | The character separating the title and the prefix / suffix. 72 | | 73 | */ 74 | separator: "|", 75 | }, 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Default meta 80 | |-------------------------------------------------------------------------- 81 | | 82 | | The default <meta> tags. 83 | | 84 | */ 85 | defaultMeta: { 86 | charSet: "utf-8", 87 | viewport: "width=device-width, initial-scale=1.0", 88 | }, 89 | }, 90 | 91 | server: { 92 | /* 93 | |-------------------------------------------------------------------------- 94 | | Streaming 95 | |-------------------------------------------------------------------------- 96 | | 97 | | Stream the rendered view to the client instead of 98 | | waiting for the full view to finish rendering. 99 | | This will enable Streaming SSR features from React 18. 100 | | 101 | */ 102 | streaming: true, 103 | }, 104 | 105 | client: { 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Limit manifest 109 | |-------------------------------------------------------------------------- 110 | | 111 | | Limit the client-side manifest to only include data required for hydration. 112 | | Disable this if you have some use case that requires 113 | | all data to be available at all times. 114 | | 115 | */ 116 | limitManifest: true, 117 | 118 | /* 119 | |-------------------------------------------------------------------------- 120 | | Build options 121 | |-------------------------------------------------------------------------- 122 | | 123 | | Allows overriding the build options used 124 | | by esbuild for bundling the client. 125 | | 126 | | Use with caution: This is only an escape hatch. 127 | | Overriding the options can break the build. 128 | | 129 | */ 130 | buildOptions: {}, 131 | }, 132 | }; 133 | 134 | export default radonisConfig; 135 | -------------------------------------------------------------------------------- /config/redis.ts: -------------------------------------------------------------------------------- 1 | import type { RedisConfig } from "@ioc:Adonis/Addons/Redis"; 2 | import Env from "@ioc:Adonis/Core/Env"; 3 | 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Redis configuration 7 | |-------------------------------------------------------------------------- 8 | | 9 | | Following is the configuration used by the Redis provider to connect to 10 | | the redis server and execute redis commands. 11 | | 12 | | Do make sure to pre-define the connections type inside `contracts/redis.ts` 13 | | file for AdonisJs to recognize connections. 14 | | 15 | | Make sure to check `contracts/redis.ts` file for defining extra connections 16 | */ 17 | const redisConfig: RedisConfig = { 18 | connection: Env.get("REDIS_CONNECTION"), 19 | 20 | connections: { 21 | /* 22 | |-------------------------------------------------------------------------- 23 | | The default connection 24 | |-------------------------------------------------------------------------- 25 | | 26 | | The main connection you want to use to execute redis commands. The same 27 | | connection will be used by the session provider, if you rely on the 28 | | redis driver. 29 | | 30 | */ 31 | local: { 32 | host: Env.get("REDIS_HOST"), 33 | port: Env.get("REDIS_PORT"), 34 | password: Env.get("REDIS_PASSWORD", ""), 35 | db: 0, 36 | keyPrefix: "", 37 | }, 38 | }, 39 | }; 40 | 41 | export default redisConfig; 42 | -------------------------------------------------------------------------------- /config/session.ts: -------------------------------------------------------------------------------- 1 | import type { SessionConfig } from "@ioc:Adonis/Addons/Session"; 2 | import Application from "@ioc:Adonis/Core/Application"; 3 | import Env from "@ioc:Adonis/Core/Env"; 4 | 5 | const sessionConfig: SessionConfig = { 6 | /* 7 | |-------------------------------------------------------------------------- 8 | | Enable/Disable sessions 9 | |-------------------------------------------------------------------------- 10 | | 11 | | Setting the following property to "false" will disable the session for the 12 | | entire application 13 | | 14 | */ 15 | enabled: true, 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Driver 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The session driver to use. You can choose between one of the following 23 | | drivers. 24 | | 25 | | - cookie (Uses signed cookies to store session values) 26 | | - file (Uses filesystem to store session values) 27 | | - redis (Uses redis. Make sure to install "@adonisjs/redis" as well) 28 | | 29 | | Note: Switching drivers will make existing sessions invalid. 30 | | 31 | */ 32 | driver: Env.get("SESSION_DRIVER"), 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Cookie name 37 | |-------------------------------------------------------------------------- 38 | | 39 | | The name of the cookie that will hold the session id. 40 | | 41 | */ 42 | cookieName: "adonis-session", 43 | 44 | /* 45 | |-------------------------------------------------------------------------- 46 | | Clear session when browser closes 47 | |-------------------------------------------------------------------------- 48 | | 49 | | Whether or not you want to destroy the session when browser closes. Setting 50 | | this value to `true` will ignore the `age`. 51 | | 52 | */ 53 | clearWithBrowser: false, 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Session age 58 | |-------------------------------------------------------------------------- 59 | | 60 | | The duration for which session stays active after no activity. A new HTTP 61 | | request to the server is considered as activity. 62 | | 63 | | The value can be a number in milliseconds or a string that must be valid 64 | | as per https://npmjs.org/package/ms package. 65 | | 66 | | Example: `2 days`, `2.5 hrs`, `1y`, `5s` and so on. 67 | | 68 | */ 69 | age: "2h", 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Cookie values 74 | |-------------------------------------------------------------------------- 75 | | 76 | | The cookie settings are used to setup the session id cookie and also the 77 | | driver will use the same values. 78 | | 79 | */ 80 | cookie: { 81 | path: "/", 82 | httpOnly: true, 83 | sameSite: false, 84 | }, 85 | 86 | /* 87 | |-------------------------------------------------------------------------- 88 | | Configuration for the file driver 89 | |-------------------------------------------------------------------------- 90 | | 91 | | The file driver needs absolute path to the directory in which sessions 92 | | must be stored. 93 | | 94 | */ 95 | file: { 96 | location: Application.tmpPath("sessions"), 97 | }, 98 | 99 | /* 100 | |-------------------------------------------------------------------------- 101 | | Redis driver 102 | |-------------------------------------------------------------------------- 103 | | 104 | | The redis connection you want session driver to use. The same connection 105 | | must be defined inside `config/redis.ts` file as well. 106 | | 107 | */ 108 | redisConnection: "local", 109 | }; 110 | 111 | export default sessionConfig; 112 | -------------------------------------------------------------------------------- /config/shield.ts: -------------------------------------------------------------------------------- 1 | import type { ShieldConfig } from "@ioc:Adonis/Addons/Shield"; 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Content Security Policy 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Content security policy filters out the origins not allowed to execute 9 | | and load resources like scripts, styles and fonts. There are wide 10 | | variety of options to choose from. 11 | */ 12 | export const csp: ShieldConfig["csp"] = { 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Enable/disable CSP 16 | |-------------------------------------------------------------------------- 17 | | 18 | | The CSP rules are disabled by default for seamless onboarding. 19 | | 20 | */ 21 | enabled: false, 22 | 23 | /* 24 | |-------------------------------------------------------------------------- 25 | | Directives 26 | |-------------------------------------------------------------------------- 27 | | 28 | | All directives are defined in camelCase and here is the list of 29 | | available directives and their possible values. 30 | | 31 | | https://content-security-policy.com 32 | | 33 | | @example 34 | | directives: { 35 | | defaultSrc: ["'self'", '@nonce', 'cdnjs.cloudflare.com'] 36 | | } 37 | | 38 | */ 39 | directives: {}, 40 | 41 | /* 42 | |-------------------------------------------------------------------------- 43 | | Report only 44 | |-------------------------------------------------------------------------- 45 | | 46 | | Setting `reportOnly=true` will not block the scripts from running and 47 | | instead report them to a URL. 48 | | 49 | */ 50 | reportOnly: false, 51 | }; 52 | 53 | /* 54 | |-------------------------------------------------------------------------- 55 | | CSRF Protection 56 | |-------------------------------------------------------------------------- 57 | | 58 | | CSRF Protection adds another layer of security by making sure, actionable 59 | | routes does have a valid token to execute an action. 60 | | 61 | */ 62 | export const csrf: ShieldConfig["csrf"] = { 63 | /* 64 | |-------------------------------------------------------------------------- 65 | | Enable/Disable CSRF 66 | |-------------------------------------------------------------------------- 67 | */ 68 | enabled: false, 69 | 70 | /* 71 | |-------------------------------------------------------------------------- 72 | | Routes to Ignore 73 | |-------------------------------------------------------------------------- 74 | | 75 | | Define an array of route patterns that you want to ignore from CSRF 76 | | validation. Make sure the route patterns are started with a leading 77 | | slash. Example: 78 | | 79 | | `/foo/bar` 80 | | 81 | | Also you can define a function that is evaluated on every HTTP Request. 82 | | ``` 83 | | exceptRoutes: ({ request }) => request.url().includes('/api') 84 | | ``` 85 | | 86 | */ 87 | exceptRoutes: [], 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Enable Sharing Token Via Cookie 92 | |-------------------------------------------------------------------------- 93 | | 94 | | When the following flag is enabled, AdonisJS will drop `XSRF-TOKEN` 95 | | cookie that frontend frameworks can read and return back as a 96 | | `X-XSRF-TOKEN` header. 97 | | 98 | | The cookie has `httpOnly` flag set to false, so it is little insecure and 99 | | can be turned off when you are not using a frontend framework making 100 | | AJAX requests. 101 | | 102 | */ 103 | enableXsrfCookie: true, 104 | 105 | /* 106 | |-------------------------------------------------------------------------- 107 | | Methods to Validate 108 | |-------------------------------------------------------------------------- 109 | | 110 | | Define an array of HTTP methods to be validated for a valid CSRF token. 111 | | 112 | */ 113 | methods: ["POST", "PUT", "PATCH", "DELETE"], 114 | }; 115 | 116 | /* 117 | |-------------------------------------------------------------------------- 118 | | DNS Prefetching 119 | |-------------------------------------------------------------------------- 120 | | 121 | | DNS prefetching allows browsers to proactively perform domain name 122 | | resolution in background. 123 | | 124 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-DNS-Prefetch-Control 125 | | 126 | */ 127 | export const dnsPrefetch: ShieldConfig["dnsPrefetch"] = { 128 | /* 129 | |-------------------------------------------------------------------------- 130 | | Enable/disable this feature 131 | |-------------------------------------------------------------------------- 132 | */ 133 | enabled: true, 134 | 135 | /* 136 | |-------------------------------------------------------------------------- 137 | | Allow or Dis-Allow Explicitly 138 | |-------------------------------------------------------------------------- 139 | | 140 | | The `enabled` boolean does not set `X-DNS-Prefetch-Control` header. However 141 | | the `allow` boolean controls the value of `X-DNS-Prefetch-Control` header. 142 | | 143 | | - When `allow = true`, then `X-DNS-Prefetch-Control = 'on'` 144 | | - When `allow = false`, then `X-DNS-Prefetch-Control = 'off'` 145 | | 146 | */ 147 | allow: true, 148 | }; 149 | 150 | /* 151 | |-------------------------------------------------------------------------- 152 | | Iframe Options 153 | |-------------------------------------------------------------------------- 154 | | 155 | | xFrame defines whether or not your website can be embedded inside an 156 | | iframe. Choose from one of the following options. 157 | | 158 | | - DENY 159 | | - SAMEORIGIN 160 | | - ALLOW-FROM http://example.com 161 | | 162 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options 163 | */ 164 | export const xFrame: ShieldConfig["xFrame"] = { 165 | enabled: true, 166 | action: "DENY", 167 | }; 168 | 169 | /* 170 | |-------------------------------------------------------------------------- 171 | | Http Strict Transport Security 172 | |-------------------------------------------------------------------------- 173 | | 174 | | A security to ensure that a browser always makes a connection over 175 | | HTTPS. 176 | | 177 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Strict-Transport-Security 178 | | 179 | */ 180 | export const hsts: ShieldConfig["hsts"] = { 181 | enabled: true, 182 | /* 183 | |-------------------------------------------------------------------------- 184 | | Max Age 185 | |-------------------------------------------------------------------------- 186 | | 187 | | Control, how long the browser should remember that a site is only to be 188 | | accessed using HTTPS. 189 | | 190 | */ 191 | maxAge: "180 days", 192 | 193 | /* 194 | |-------------------------------------------------------------------------- 195 | | Include Subdomains 196 | |-------------------------------------------------------------------------- 197 | | 198 | | Apply rules on the subdomains as well. 199 | | 200 | */ 201 | includeSubDomains: true, 202 | 203 | /* 204 | |-------------------------------------------------------------------------- 205 | | Preloading 206 | |-------------------------------------------------------------------------- 207 | | 208 | | Google maintains a service to register your domain and it will preload 209 | | the HSTS policy. Learn more https://hstspreload.org/ 210 | | 211 | */ 212 | preload: false, 213 | }; 214 | 215 | /* 216 | |-------------------------------------------------------------------------- 217 | | No Sniff 218 | |-------------------------------------------------------------------------- 219 | | 220 | | Browsers have a habit of sniffing content-type of a response. Which means 221 | | files with .txt extension containing Javascript code will be executed as 222 | | Javascript. You can disable this behavior by setting nosniff to false. 223 | | 224 | | Learn more at https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Content-Type-Options 225 | | 226 | */ 227 | export const contentTypeSniffing: ShieldConfig["contentTypeSniffing"] = { 228 | enabled: true, 229 | }; 230 | -------------------------------------------------------------------------------- /config/static.ts: -------------------------------------------------------------------------------- 1 | import type { AssetsConfig } from "@ioc:Adonis/Core/Static"; 2 | 3 | const staticConfig: AssetsConfig = { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Enabled 7 | |-------------------------------------------------------------------------- 8 | | 9 | | A boolean to enable or disable serving static files. The static files 10 | | are served from the `public` directory inside the application root. 11 | | However, you can override the default path inside `.adonisrc.json` 12 | | file. 13 | | 14 | | 15 | */ 16 | enabled: true, 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Handling Dot Files 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Decide how you want the static assets server to handle the `dotfiles`. 24 | | By default, we ignore them as if they don't exists. However, you 25 | | can choose between one of the following options. 26 | | 27 | | - ignore: Behave as if the file doesn't exists. Results in 404. 28 | | - deny: Deny access to the file. Results in 403. 29 | | - allow: Serve the file contents 30 | | 31 | */ 32 | dotFiles: "ignore", 33 | 34 | /* 35 | |-------------------------------------------------------------------------- 36 | | Generating Etag 37 | |-------------------------------------------------------------------------- 38 | | 39 | | Handle whether or not to generate etags for the files. Etag allows browser 40 | | to utilize the cache when file hasn't been changed. 41 | | 42 | */ 43 | etag: true, 44 | 45 | /* 46 | |-------------------------------------------------------------------------- 47 | | Set Last Modified 48 | |-------------------------------------------------------------------------- 49 | | 50 | | Whether or not to set the `Last-Modified` header in the response. Uses 51 | | the file system's last modified value. 52 | | 53 | */ 54 | lastModified: true, 55 | 56 | /* 57 | |-------------------------------------------------------------------------- 58 | | Max age 59 | |-------------------------------------------------------------------------- 60 | | 61 | | Set the value for the max-age directive. Set a higher value in production 62 | | if you fingerprint your assets. 63 | | 64 | | Learn more: https://docs.adonisjs.com/guides/deployment#serving-static-assets 65 | | 66 | */ 67 | maxAge: 0, 68 | 69 | /* 70 | |-------------------------------------------------------------------------- 71 | | Immutable 72 | |-------------------------------------------------------------------------- 73 | | 74 | | Set the immutable directive. Set it to `true` if the assets are generated 75 | | with a fingerprint. In others words the file name changes when the file 76 | | contents change. 77 | | 78 | */ 79 | immutable: false, 80 | }; 81 | 82 | export default staticConfig; 83 | -------------------------------------------------------------------------------- /contracts/auth.ts: -------------------------------------------------------------------------------- 1 | import type User from "App/Models/User"; 2 | 3 | declare module "@ioc:Adonis/Addons/Auth" { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Providers 7 | |-------------------------------------------------------------------------- 8 | | 9 | | The providers are used to fetch users. The Auth module comes pre-bundled 10 | | with two providers that are `Lucid` and `Database`. Both uses database 11 | | to fetch user details. 12 | | 13 | | You can also create and register your own custom providers. 14 | | 15 | */ 16 | interface ProvidersList { 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | User Provider 20 | |-------------------------------------------------------------------------- 21 | | 22 | | The following provider uses Lucid models as a driver for fetching user 23 | | details from the database for authentication. 24 | | 25 | | You can create multiple providers using the same underlying driver with 26 | | different Lucid models. 27 | | 28 | */ 29 | user: { 30 | implementation: LucidProviderContract<typeof User>; 31 | config: LucidProviderConfig<typeof User>; 32 | }; 33 | } 34 | 35 | /* 36 | |-------------------------------------------------------------------------- 37 | | Guards 38 | |-------------------------------------------------------------------------- 39 | | 40 | | The guards are used for authenticating users using different drivers. 41 | | The auth module comes with 3 different guards. 42 | | 43 | | - SessionGuardContract 44 | | - BasicAuthGuardContract 45 | | - OATGuardContract ( Opaque access token ) 46 | | 47 | | Every guard needs a provider for looking up users from the database. 48 | | 49 | */ 50 | interface GuardsList { 51 | /* 52 | |-------------------------------------------------------------------------- 53 | | Web Guard 54 | |-------------------------------------------------------------------------- 55 | | 56 | | The web guard uses sessions for maintaining user login state. It uses 57 | | the `user` provider for fetching user details. 58 | | 59 | */ 60 | web: { 61 | implementation: SessionGuardContract<"user", "web">; 62 | config: SessionGuardConfig<"user">; 63 | }; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /contracts/bouncer.ts: -------------------------------------------------------------------------------- 1 | import type { actions, policies } from "../start/bouncer"; 2 | 3 | declare module "@ioc:Adonis/Addons/Bouncer" { 4 | type ApplicationActions = ExtractActionsTypes<typeof actions>; 5 | type ApplicationPolicies = ExtractPoliciesTypes<typeof policies>; 6 | 7 | interface ActionsList extends ApplicationActions {} 8 | interface PoliciesList extends ApplicationPolicies {} 9 | } 10 | -------------------------------------------------------------------------------- /contracts/drive.ts: -------------------------------------------------------------------------------- 1 | declare module "@ioc:Adonis/Core/Drive" { 2 | interface DisksList { 3 | local: { 4 | config: LocalDriverConfig; 5 | implementation: LocalDriverContract; 6 | }; 7 | s3: { 8 | config: S3DriverConfig; 9 | implementation: S3DriverContract; 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/env.ts: -------------------------------------------------------------------------------- 1 | declare module "@ioc:Adonis/Core/Env" { 2 | // eslint-disable-next-line @typescript-eslint/consistent-type-imports 3 | type CustomTypes = typeof import("../env").default; 4 | interface EnvTypes extends CustomTypes {} 5 | } 6 | -------------------------------------------------------------------------------- /contracts/events.ts: -------------------------------------------------------------------------------- 1 | declare module "@ioc:Adonis/Core/Event" { 2 | /* 3 | |-------------------------------------------------------------------------- 4 | | Define typed events 5 | |-------------------------------------------------------------------------- 6 | | 7 | | You can define types for events inside the following interface and 8 | | AdonisJS will make sure that all listeners and emit calls adheres 9 | | to the defined types. 10 | | 11 | | For example: 12 | | 13 | | interface EventsList { 14 | | 'new:user': UserModel 15 | | } 16 | | 17 | | Now calling `Event.emit('new:user')` will statically ensure that passed value is 18 | | an instance of the the UserModel only. 19 | | 20 | */ 21 | interface EventsList {} 22 | } 23 | -------------------------------------------------------------------------------- /contracts/hash.ts: -------------------------------------------------------------------------------- 1 | declare module "@ioc:Adonis/Core/Hash" { 2 | interface HashersList { 3 | bcrypt: { 4 | config: BcryptConfig; 5 | implementation: BcryptContract; 6 | }; 7 | argon: { 8 | config: ArgonConfig; 9 | implementation: ArgonContract; 10 | }; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /contracts/mail.ts: -------------------------------------------------------------------------------- 1 | declare module "@ioc:Adonis/Addons/Mail" { 2 | import type { MailDrivers } from "@ioc:Adonis/Addons/Mail"; 3 | 4 | interface MailersList { 5 | ses: MailDrivers["ses"]; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /contracts/radonis.ts: -------------------------------------------------------------------------------- 1 | import type User from "App/Models/User"; 2 | 3 | declare module "@microeinhundert/radonis-types" { 4 | /* 5 | |-------------------------------------------------------------------------- 6 | | Define typed globals 7 | |-------------------------------------------------------------------------- 8 | | 9 | | You can define types for globals inside the following interface and 10 | | Radonis will make sure that all usages of globals adhere 11 | | to the defined types. 12 | | 13 | | For example: 14 | | 15 | | interface Globals { 16 | | hello: string 17 | | } 18 | | 19 | | Now calling `radonis.withGlobals({ hello: 'Hello world!' })` will statically ensure correct types. 20 | | 21 | */ 22 | interface Globals { 23 | authenticatedUser?: User; 24 | csrfToken?: string; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /contracts/redis.ts: -------------------------------------------------------------------------------- 1 | declare module "@ioc:Adonis/Addons/Redis" { 2 | interface RedisConnectionsList { 3 | local: RedisConnectionConfig; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /contracts/tests.ts: -------------------------------------------------------------------------------- 1 | import "@japa/runner"; 2 | 3 | declare module "@japa/runner" { 4 | interface TestContext { 5 | // Extend context 6 | } 7 | 8 | interface Test<TestData> { 9 | // Extend test 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /database/factories/GardenFactory.ts: -------------------------------------------------------------------------------- 1 | import Factory from "@ioc:Adonis/Lucid/Factory"; 2 | import Garden from "App/Models/Garden"; 3 | 4 | import UserFactory from "./UserFactory"; 5 | 6 | export default Factory.define(Garden, async ({ faker }) => { 7 | const user = await UserFactory.create(); 8 | 9 | return { 10 | name: faker.random.words(3), 11 | zip: faker.address.zipCode(), 12 | city: faker.address.cityName(), 13 | userId: user.id, 14 | }; 15 | }).build(); 16 | -------------------------------------------------------------------------------- /database/factories/UserFactory.ts: -------------------------------------------------------------------------------- 1 | import Factory from "@ioc:Adonis/Lucid/Factory"; 2 | import User from "App/Models/User"; 3 | 4 | export default Factory.define(User, ({ faker }) => { 5 | return { 6 | firstName: faker.name.firstName(), 7 | lastName: faker.name.lastName(), 8 | email: faker.internet.email(), 9 | password: faker.internet.password(), 10 | }; 11 | }).build(); 12 | -------------------------------------------------------------------------------- /database/factories/index.ts: -------------------------------------------------------------------------------- 1 | // import Factory from '@ioc:Adonis/Lucid/Factory' 2 | -------------------------------------------------------------------------------- /database/migrations/1649328692013_users.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from "@ioc:Adonis/Lucid/Schema"; 2 | 3 | export default class UsersSchema extends BaseSchema { 4 | protected tableName = "users"; 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments("id").primary(); 9 | table.string("first_name", 255).notNullable(); 10 | table.string("last_name", 255).notNullable(); 11 | table.string("email", 255).notNullable(); 12 | table.string("password", 180).notNullable(); 13 | table.string("remember_me_token").nullable(); 14 | table.timestamp("created_at", { useTz: true }).notNullable(); 15 | table.timestamp("updated_at", { useTz: true }).notNullable(); 16 | }); 17 | } 18 | 19 | public async down() { 20 | this.schema.dropTable(this.tableName); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /database/migrations/1651250696290_gardens.ts: -------------------------------------------------------------------------------- 1 | import BaseSchema from "@ioc:Adonis/Lucid/Schema"; 2 | 3 | export default class Gardens extends BaseSchema { 4 | protected tableName = "gardens"; 5 | 6 | public async up() { 7 | this.schema.createTable(this.tableName, (table) => { 8 | table.increments("id"); 9 | table.string("name", 255).notNullable(); 10 | table.string("zip", 255).notNullable(); 11 | table.string("city", 255).notNullable(); 12 | table.integer("user_id").unsigned().references("users.id").onDelete("CASCADE"); // delete garden when user is deleted 13 | table.timestamp("created_at", { useTz: true }); 14 | table.timestamp("updated_at", { useTz: true }); 15 | }); 16 | } 17 | 18 | public async down() { 19 | this.schema.dropTable(this.tableName); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /database/seeders/Garden.ts: -------------------------------------------------------------------------------- 1 | import BaseSeeder from "@ioc:Adonis/Lucid/Seeder"; 2 | import GardenFactory from "Database/factories/GardenFactory"; 3 | 4 | export default class extends BaseSeeder { 5 | public async run() { 6 | await GardenFactory.createMany(20); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | redis: 5 | image: "redis:alpine" 6 | hostname: redis 7 | container_name: redis 8 | ports: 9 | - "${REDIS_PORT:-6379}:6379" 10 | healthcheck: 11 | test: ["CMD", "redis-cli", "ping"] 12 | retries: 3 13 | timeout: 5s 14 | volumes: 15 | - "sailredis:/data" 16 | networks: 17 | - sail 18 | 19 | pgsql: 20 | image: "postgres:13" 21 | container_name: pgsql 22 | ports: 23 | - "${PG_PORT:-5432}:5432" 24 | environment: 25 | PGPASSWORD: "${PG_PASSWORD:-secret}" 26 | POSTGRES_DB: "${PG_DB_NAME:-default}" 27 | POSTGRES_USER: "${PG_USER?:err}" 28 | POSTGRES_PASSWORD: "${PG_PASSWORD:-secret}" 29 | volumes: 30 | - "sailpgsql:/var/lib/postgresql/data" 31 | networks: 32 | - sail 33 | healthcheck: 34 | test: ["CMD", "pg_isready", "-q", "-d", "${PG_DB_NAME:-default}", "-U", "${PG_USER}"] 35 | retries: 3 36 | timeout: 5s 37 | 38 | minio: 39 | image: "minio/minio:latest" 40 | container_name: "minio" 41 | ports: 42 | - "${MINIO_PORT:-9000}:9000" 43 | - "${MINIO_CONSOLE_PORT:-8900}:8900" 44 | environment: 45 | MINIO_ROOT_USER: "sail" 46 | MINIO_ROOT_PASSWORD: "password" 47 | volumes: 48 | - "sailminio:/data/minio" 49 | networks: 50 | - sail 51 | command: minio server /data/minio --console-address ":8900" 52 | healthcheck: 53 | test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] 54 | retries: 3 55 | timeout: 5s 56 | 57 | mailhog: 58 | image: "mailhog/mailhog:latest" 59 | container_name: "mailhog" 60 | ports: 61 | - "${MAILHOG_PORT:-1025}:1025" 62 | - "${MAILHOG_DASHBOARD_PORT:-8025}:8025" 63 | networks: 64 | - sail 65 | 66 | networks: 67 | sail: 68 | driver: bridge 69 | 70 | volumes: 71 | sailredis: 72 | sailpgsql: 73 | sailminio: 74 | -------------------------------------------------------------------------------- /env.ts: -------------------------------------------------------------------------------- 1 | import Env from "@ioc:Adonis/Core/Env"; 2 | 3 | export default Env.rules({ 4 | HOST: Env.schema.string({ format: "host" }), 5 | PORT: Env.schema.number(), 6 | APP_KEY: Env.schema.string(), 7 | APP_NAME: Env.schema.string(), 8 | SESSION_DRIVER: Env.schema.string(), 9 | DRIVE_DISK: Env.schema.enum(["local", "s3"] as const), 10 | NODE_ENV: Env.schema.enum(["development", "production", "test"] as const), 11 | DB_CONNECTION: Env.schema.string(), 12 | 13 | PG_HOST: Env.schema.string({ format: "host" }), 14 | PG_PORT: Env.schema.number(), 15 | PG_USER: Env.schema.string(), 16 | PG_PASSWORD: Env.schema.string.optional(), 17 | PG_DB_NAME: Env.schema.string(), 18 | 19 | REDIS_CONNECTION: Env.schema.enum(["local"] as const), 20 | REDIS_HOST: Env.schema.string({ format: "host" }), 21 | REDIS_PORT: Env.schema.number(), 22 | REDIS_PASSWORD: Env.schema.string.optional(), 23 | 24 | SES_ACCESS_KEY: Env.schema.string(), 25 | SES_ACCESS_SECRET: Env.schema.string(), 26 | SES_REGION: Env.schema.string(), 27 | 28 | S3_KEY: Env.schema.string(), 29 | S3_SECRET: Env.schema.string(), 30 | S3_BUCKET: Env.schema.string(), 31 | S3_REGION: Env.schema.string(), 32 | S3_ENDPOINT: Env.schema.string.optional(), 33 | }); 34 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | import type { ComponentType } from "react"; 2 | 3 | declare global { 4 | type HTMLProps<Tag extends keyof JSX.IntrinsicElements> = JSX.IntrinsicElements[Tag]; 5 | 6 | type IconComponent = ComponentType<HTMLProps<"svg"> & { className?: string }>; 7 | } 8 | 9 | export {}; 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "radonis-example-application", 3 | "version": "1.0.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "concurrently 'npm:dev:*'", 7 | "dev:client": "node ace build:client --watch", 8 | "dev:server": "node ace serve --watch", 9 | "build": "npm run build:server && npm run build:client", 10 | "build:client": "node ace build:client --production", 11 | "build:server": "node ace build --production", 12 | "test": "node ace test", 13 | "start": "node server.js", 14 | "lint": "eslint .", 15 | "format": "prettier --write ." 16 | }, 17 | "devDependencies": { 18 | "@adonisjs/assembler": "^5.9.5", 19 | "@japa/preset-adonis": "^1.2.0", 20 | "@japa/runner": "^2.5.1", 21 | "@types/react": "^18.0.28", 22 | "@types/react-dom": "^18.0.11", 23 | "adonis-preset-ts": "^2.1.0", 24 | "concurrently": "^7.6.0", 25 | "eslint": "^8.35.0", 26 | "eslint-config-prettier": "^8.7.0", 27 | "eslint-plugin-adonis": "^2.1.1", 28 | "eslint-plugin-jsx-a11y": "^6.7.1", 29 | "eslint-plugin-prettier": "^4.2.1", 30 | "eslint-plugin-react": "^7.32.2", 31 | "eslint-plugin-react-hooks": "^4.6.0", 32 | "eslint-plugin-simple-import-sort": "^10.0.0", 33 | "openapi-types": "^12.1.0", 34 | "pino-pretty": "^9.4.0", 35 | "prettier": "^2.8.4", 36 | "prettier-plugin-tailwindcss": "^0.2.4", 37 | "typescript": "4.9.5", 38 | "youch": "^3.2.3", 39 | "youch-terminal": "^2.2.0" 40 | }, 41 | "dependencies": { 42 | "@adonisjs/auth": "^8.2.3", 43 | "@adonisjs/bouncer": "^2.3.0", 44 | "@adonisjs/core": "^5.9.0", 45 | "@adonisjs/drive-s3": "^1.3.2", 46 | "@adonisjs/i18n": "^1.5.6", 47 | "@adonisjs/lucid": "^18.3.0", 48 | "@adonisjs/mail": "^8.1.2", 49 | "@adonisjs/redis": "^7.3.2", 50 | "@adonisjs/repl": "^3.1.11", 51 | "@adonisjs/session": "^6.4.0", 52 | "@adonisjs/shield": "^7.1.0", 53 | "@headlessui/react": "^1.7.13", 54 | "@heroicons/react": "^2.0.16", 55 | "@microeinhundert/radonis": "^5.0.4", 56 | "@microeinhundert/radonis-server": "^5.0.4", 57 | "@microeinhundert/radonis-unocss": "^5.0.4", 58 | "aws-sdk": "^2.1329.0", 59 | "isbot": "^3.6.6", 60 | "luxon": "^3.3.0", 61 | "pg": "^8.10.0", 62 | "phc-argon2": "^1.1.4", 63 | "proxy-addr": "^2.0.7", 64 | "react": "^18.2.0", 65 | "react-dom": "^18.2.0", 66 | "reflect-metadata": "^0.1.13", 67 | "source-map-support": "^0.5.21" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /providers/AppProvider.ts: -------------------------------------------------------------------------------- 1 | import type { ApplicationContract } from "@ioc:Adonis/Core/Application"; 2 | 3 | export default class AppProvider { 4 | constructor(protected app: ApplicationContract) {} 5 | 6 | public register() { 7 | // Register your own bindings 8 | } 9 | 10 | public async boot() { 11 | // IoC container is ready 12 | } 13 | 14 | public async ready() { 15 | // App is ready 16 | } 17 | 18 | public async shutdown() { 19 | // Cleanup, since app is going down 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microeinhundert/radonis-example-application/d62099061b8cd71541608fb8fb03fe9aeb172894/public/favicon.ico -------------------------------------------------------------------------------- /resources/components/Auth/SignInForm.island.tsx: -------------------------------------------------------------------------------- 1 | import ExclamationCircleIcon from "@heroicons/react/24/outline/ExclamationCircleIcon"; 2 | import { Form, island, useFlashMessages, useI18n, useUrlBuilder } from "@microeinhundert/radonis"; 3 | 4 | import Button from "../Button.island"; 5 | import Checkbox from "../Checkbox.island"; 6 | import CsrfField from "../CsrfField"; 7 | import Input from "../Input.island"; 8 | import Link from "../Link"; 9 | 10 | /* 11 | * Sign In Form 12 | */ 13 | function SignInForm() { 14 | const { formatMessage$ } = useI18n(); 15 | const { make$ } = useUrlBuilder(); 16 | const { has$, get$ } = useFlashMessages(); 17 | 18 | const messages = { 19 | email: { 20 | label: formatMessage$("auth.signIn.form.email.label"), 21 | }, 22 | password: { 23 | label: formatMessage$("auth.signIn.form.password.label"), 24 | }, 25 | rememberMe: { 26 | label: formatMessage$("auth.signIn.form.rememberMe.label"), 27 | }, 28 | actions: { 29 | submit: formatMessage$("auth.signIn.form.actions.submit"), 30 | }, 31 | signUpLinkLabel: formatMessage$("auth.signIn.form.signUpLinkLabel"), 32 | }; 33 | 34 | return ( 35 | <Form action$="signIn" method="post" noValidate> 36 | <div className="flex flex-col gap-5"> 37 | <CsrfField /> 38 | <Input 39 | autoComplete="email" 40 | label={messages.email.label} 41 | name="email" 42 | type="email" 43 | required 44 | /> 45 | <Input 46 | autoComplete="new-password" 47 | label={messages.password.label} 48 | name="password" 49 | type="password" 50 | required 51 | /> 52 | {has$("errors.invalidEmailOrPassword") && ( 53 | <div className="my-4 flex flex-col items-center gap-4 rounded-lg bg-red-50 p-4 text-center text-sm font-medium text-red-600 sm:p-6"> 54 | <ExclamationCircleIcon className="h-6 w-6" /> 55 | <span className="block max-w-[30ch]">{get$("errors.invalidEmailOrPassword")}</span> 56 | </div> 57 | )} 58 | <Checkbox label={messages.rememberMe.label} name="rememberMe" /> 59 | <Button className="mt-4" type="submit"> 60 | {messages.actions.submit} 61 | </Button> 62 | <div className="text-center text-sm"> 63 | <Link href={make$("signUp")}>{messages.signUpLinkLabel}</Link> 64 | </div> 65 | </div> 66 | </Form> 67 | ); 68 | } 69 | 70 | export default island("SignInForm", SignInForm); 71 | -------------------------------------------------------------------------------- /resources/components/Auth/SignUpForm.island.tsx: -------------------------------------------------------------------------------- 1 | import { Form, island, useI18n, useUrlBuilder } from "@microeinhundert/radonis"; 2 | 3 | import Button from "../Button.island"; 4 | import CsrfField from "../CsrfField"; 5 | import Input from "../Input.island"; 6 | import Link from "../Link"; 7 | 8 | /* 9 | * Sign Up Form 10 | */ 11 | function SignUpForm() { 12 | const { formatMessage$ } = useI18n(); 13 | const { make$ } = useUrlBuilder(); 14 | 15 | const messages = { 16 | firstName: { 17 | label: formatMessage$("auth.signUp.form.firstName.label"), 18 | }, 19 | lastName: { 20 | label: formatMessage$("auth.signUp.form.lastName.label"), 21 | }, 22 | email: { 23 | label: formatMessage$("auth.signUp.form.email.label"), 24 | }, 25 | password: { 26 | label: formatMessage$("auth.signUp.form.password.label"), 27 | }, 28 | passwordConfirmation: { 29 | label: formatMessage$("auth.signUp.form.passwordConfirmation.label"), 30 | }, 31 | actions: { 32 | submit: formatMessage$("auth.signUp.form.actions.submit"), 33 | }, 34 | signInLinkLabel: formatMessage$("auth.signUp.form.signInLinkLabel"), 35 | }; 36 | 37 | return ( 38 | <Form action$="signUp" method="post" noValidate> 39 | <div className="flex flex-col gap-5"> 40 | <CsrfField /> 41 | <Input 42 | autoComplete="given-name" 43 | label={messages.firstName.label} 44 | name="firstName" 45 | placeholder="John" 46 | type="text" 47 | required 48 | /> 49 | <Input 50 | autoComplete="family-name" 51 | label={messages.lastName.label} 52 | name="lastName" 53 | placeholder="Doe" 54 | type="text" 55 | required 56 | /> 57 | <Input 58 | autoComplete="email" 59 | label={messages.email.label} 60 | name="email" 61 | placeholder="john.doe@example.com" 62 | type="email" 63 | required 64 | /> 65 | <Input 66 | autoComplete="new-password" 67 | label={messages.password.label} 68 | name="password" 69 | type="password" 70 | required 71 | /> 72 | <Input 73 | autoComplete="new-password" 74 | label={messages.passwordConfirmation.label} 75 | name="passwordConfirmation" 76 | type="password" 77 | required 78 | /> 79 | <Button className="mt-4" type="submit"> 80 | {messages.actions.submit} 81 | </Button> 82 | <div className="text-center text-sm"> 83 | <Link href={make$("signIn")}>{messages.signInLinkLabel}</Link> 84 | </div> 85 | </div> 86 | </Form> 87 | ); 88 | } 89 | 90 | export default island("SignUpForm", SignUpForm); 91 | -------------------------------------------------------------------------------- /resources/components/Button.island.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/anchor-has-content */ 2 | import type { RouteParams, RouteQueryParams } from "@microeinhundert/radonis"; 3 | import { island } from "@microeinhundert/radonis"; 4 | import { useUrlBuilder } from "@microeinhundert/radonis"; 5 | 6 | import { clsx } from "../utils/string"; 7 | 8 | /* 9 | * Shared 10 | */ 11 | export enum ButtonColor { 12 | Emerald = "emerald", 13 | Red = "red", 14 | White = "white", 15 | WhiteDanger = "white-danger", 16 | } 17 | 18 | type ButtonTag = "a" | "button"; 19 | 20 | type ButtonBaseProps<Tag extends ButtonTag> = HTMLProps<Tag> & { 21 | small?: boolean; 22 | round?: boolean; 23 | disabled?: boolean; 24 | icon?: IconComponent; 25 | color?: ButtonColor; 26 | }; 27 | 28 | function getButtonColorClasses(color: ButtonColor) { 29 | return { 30 | [ButtonColor.Emerald]: `border-emerald-600 text-white bg-emerald-600 hover:bg-emerald-700 focus:ring-emerald-500`, 31 | [ButtonColor.Red]: `border-red-600 text-white bg-red-600 hover:bg-red-700 focus:ring-red-500`, 32 | [ButtonColor.White]: `border-gray-200 text-gray-700 bg-white hover:bg-gray-50 focus:ring-emerald-500`, 33 | [ButtonColor.WhiteDanger]: `border-gray-200 text-gray-700 bg-white hover:bg-red-600 hover:text-white focus:ring-red-500`, 34 | }[color]; 35 | } 36 | 37 | function useButtonProps<Tag extends ButtonTag>(props: ButtonBaseProps<Tag>) { 38 | const { 39 | children, 40 | small, 41 | round, 42 | disabled, 43 | icon, 44 | color = ButtonColor.Emerald, 45 | className, 46 | ...propsWeDontControl 47 | } = props; 48 | 49 | return { 50 | getProps: (defaults?: HTMLProps<Tag>) => ({ 51 | ...defaults, 52 | ...propsWeDontControl, 53 | className: clsx( 54 | "flex justify-center items-center gap-2", 55 | "border font-medium transition", 56 | "focus:outline-none focus:ring-2 focus:ring-offset-2", 57 | children ? (small ? "px-3 py-1" : "px-4 py-2") : small ? "px-1 py-1" : "px-2 py-2", 58 | small ? "text-xs" : "text-sm", 59 | round ? "rounded-full" : "rounded-lg", 60 | disabled && "opacity-25 pointer-events-none", 61 | getButtonColorClasses(color), 62 | className 63 | ), 64 | }), 65 | ...props, 66 | }; 67 | } 68 | 69 | /* 70 | * Button Content 71 | */ 72 | function ButtonContent<Tag extends ButtonTag>({ 73 | children, 74 | title, 75 | small, 76 | icon: Icon, 77 | }: ButtonBaseProps<Tag>) { 78 | return ( 79 | <> 80 | {children ?? (title && <div className="sr-only">{title}</div>)} 81 | {Icon && <Icon className={clsx(small ? "h-3 w-3" : "h-5 w-5")} />} 82 | </> 83 | ); 84 | } 85 | 86 | /* 87 | * Anchor Button 88 | */ 89 | interface AnchorButtonProps extends ButtonBaseProps<"a"> {} 90 | 91 | function AnchorButton(props: AnchorButtonProps) { 92 | const { getProps, disabled, children } = useButtonProps<"a">(props); 93 | 94 | return ( 95 | <a {...getProps({ "aria-disabled": disabled ? "true" : undefined })}> 96 | <ButtonContent<"a"> {...props}>{children}</ButtonContent> 97 | </a> 98 | ); 99 | } 100 | 101 | /* 102 | * Link Button 103 | */ 104 | interface LinkButtonProps extends ButtonBaseProps<"a"> { 105 | href?: never; 106 | to$: string; 107 | params?: RouteParams; 108 | queryParams?: RouteQueryParams; 109 | } 110 | 111 | function LinkButton({ to$, params, queryParams, ...restProps }: LinkButtonProps) { 112 | const { getProps, disabled, children } = useButtonProps<"a">(restProps); 113 | const { make$ } = useUrlBuilder(); 114 | 115 | return ( 116 | <a 117 | {...getProps({ 118 | "aria-disabled": disabled ? "true" : undefined, 119 | "href": make$(to$, { params, queryParams }), 120 | })} 121 | > 122 | <ButtonContent<"a"> {...restProps}>{children}</ButtonContent> 123 | </a> 124 | ); 125 | } 126 | 127 | /* 128 | * Button 129 | */ 130 | interface ButtonProps extends ButtonBaseProps<"button"> {} 131 | 132 | function Button(props: ButtonProps) { 133 | const { getProps, disabled, children } = useButtonProps<"button">(props); 134 | 135 | return ( 136 | <button {...getProps({ type: "button", disabled })}> 137 | <ButtonContent<"button"> {...props}>{children}</ButtonContent> 138 | </button> 139 | ); 140 | } 141 | 142 | Button.Anchor = AnchorButton; 143 | Button.Link = LinkButton; 144 | 145 | export default island("Button", Button); 146 | -------------------------------------------------------------------------------- /resources/components/Card.island.tsx: -------------------------------------------------------------------------------- 1 | import { island } from "@microeinhundert/radonis"; 2 | import type { ReactNode } from "react"; 3 | 4 | import { clsx } from "../utils/string"; 5 | 6 | /* 7 | * Card Head 8 | */ 9 | interface CardHeadProps { 10 | className?: string; 11 | children: ReactNode; 12 | actions?: ReactNode; 13 | } 14 | 15 | function CardHead({ className, children, actions }: CardHeadProps) { 16 | return ( 17 | <div 18 | className={clsx( 19 | "flex flex-wrap items-center justify-between border-b border-gray-200 p-4 sm:flex-nowrap", 20 | className 21 | )} 22 | > 23 | <h3 className="flex flex-1 items-center gap-4 truncate font-medium">{children}</h3> 24 | {actions && <div className="ml-4 flex flex-shrink-0 gap-2">{actions}</div>} 25 | </div> 26 | ); 27 | } 28 | 29 | /* 30 | * Card Body 31 | */ 32 | interface CardBodyProps { 33 | className?: string; 34 | children: ReactNode; 35 | } 36 | 37 | function CardBody({ className, children }: CardBodyProps) { 38 | return <div className={clsx("p-4", className)}>{children}</div>; 39 | } 40 | 41 | /* 42 | * Card 43 | */ 44 | interface CardProps { 45 | className?: string; 46 | children: ReactNode; 47 | } 48 | 49 | function Card({ className, children }: CardProps) { 50 | return ( 51 | <div 52 | className={clsx( 53 | "overflow-hidden rounded-md border border-gray-200 bg-white text-gray-900 shadow-sm", 54 | className 55 | )} 56 | > 57 | {children} 58 | </div> 59 | ); 60 | } 61 | 62 | Card.Head = CardHead; 63 | Card.Body = CardBody; 64 | 65 | export default island("Card", Card); 66 | -------------------------------------------------------------------------------- /resources/components/Checkbox.island.tsx: -------------------------------------------------------------------------------- 1 | import { island } from "@microeinhundert/radonis"; 2 | 3 | import { useFormField } from "../hooks/useFormField"; 4 | import { clsx } from "../utils/string"; 5 | 6 | /* 7 | * Checkbox 8 | */ 9 | interface CheckboxProps extends HTMLProps<"input"> { 10 | type?: never; 11 | name: string; 12 | label: string; 13 | description?: string; 14 | } 15 | 16 | function Checkbox({ className, ...restProps }: CheckboxProps) { 17 | const field = useFormField(restProps); 18 | 19 | return ( 20 | <div> 21 | <div className="relative flex items-start gap-3"> 22 | <div className="flex h-5 items-center"> 23 | <input 24 | {...field.getInputProps({ 25 | type: "checkbox", 26 | value: "true", 27 | defaultChecked: !!field.value, 28 | })} 29 | className={clsx( 30 | "h-4 w-4 rounded transition", 31 | field.error ? "text-red-600" : "text-emerald-600", 32 | field.error ? "border-red-300" : "border-gray-300", 33 | field.error ? "focus:ring-red-500" : "focus:ring-emerald-500", 34 | className 35 | )} 36 | /> 37 | </div> 38 | <div className="text-sm"> 39 | <label {...field.getLabelProps()} className="font-medium text-gray-700"> 40 | {field.label} 41 | </label> 42 | {field.description && ( 43 | <span {...field.getDescriptionProps()} className="text-gray-500"> 44 | {field.description} 45 | </span> 46 | )} 47 | </div> 48 | </div> 49 | {field.error && ( 50 | <span {...field.getErrorProps()} className="mt-2 block text-sm text-red-600"> 51 | {field.error} 52 | </span> 53 | )} 54 | </div> 55 | ); 56 | } 57 | 58 | export default island("Checkbox", Checkbox); 59 | -------------------------------------------------------------------------------- /resources/components/CsrfField.tsx: -------------------------------------------------------------------------------- 1 | import { useCsrfToken } from "../hooks/useCsrfToken"; 2 | 3 | /* 4 | * CSRF Field 5 | */ 6 | function CsrfField() { 7 | const csrfToken = useCsrfToken(); 8 | 9 | return csrfToken ? <input name="_csrf" type="hidden" value={csrfToken} /> : null; 10 | } 11 | 12 | export default CsrfField; 13 | -------------------------------------------------------------------------------- /resources/components/Fallback.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Fallback 3 | */ 4 | interface FallbackProps { 5 | icon?: IconComponent; 6 | headline: string; 7 | text?: string; 8 | } 9 | 10 | function Fallback({ icon: Icon, headline, text }: FallbackProps) { 11 | return ( 12 | <div className="flex h-full items-center justify-center"> 13 | <div className="my-16 space-y-4 text-center text-gray-900"> 14 | {Icon && <Icon className="mx-auto w-60 max-w-full" />} 15 | <h2 className="text-3xl font-bold">{headline}</h2> 16 | {text && <p className="max-w-xl text-sm leading-6 text-gray-500">{text}</p>} 17 | </div> 18 | </div> 19 | ); 20 | } 21 | 22 | export default Fallback; 23 | -------------------------------------------------------------------------------- /resources/components/Gardens/GardenForm.island.tsx: -------------------------------------------------------------------------------- 1 | import { Form, island, token$, useI18n } from "@microeinhundert/radonis"; 2 | import type Garden from "App/Models/Garden"; 3 | 4 | import Button from "../Button.island"; 5 | import CsrfField from "../CsrfField"; 6 | import Input from "../Input.island"; 7 | 8 | /* 9 | * Garden Form 10 | */ 11 | interface GardenFormProps { 12 | garden?: Garden; 13 | } 14 | 15 | function GardenForm({ garden }: GardenFormProps) { 16 | const { formatMessage$ } = useI18n(); 17 | 18 | const messages = { 19 | name: { 20 | label: formatMessage$("gardens.form.name.label"), 21 | }, 22 | zip: { 23 | label: formatMessage$("gardens.form.zip.label"), 24 | }, 25 | city: { 26 | label: formatMessage$("gardens.form.city.label"), 27 | }, 28 | actions: { 29 | create: formatMessage$("gardens.form.actions.create"), 30 | update: formatMessage$("gardens.form.actions.update"), 31 | }, 32 | }; 33 | 34 | return ( 35 | <Form 36 | action$={garden ? token$("gardens.update") : token$("gardens.store")} 37 | method={garden ? "put" : "post"} 38 | params={garden ? { id: garden.id } : undefined} 39 | noValidate 40 | > 41 | <div className="flex flex-col gap-5"> 42 | <CsrfField /> 43 | <Input 44 | defaultValue={garden?.name} 45 | label={messages.name.label} 46 | name="name" 47 | type="text" 48 | required 49 | /> 50 | <Input 51 | defaultValue={garden?.zip} 52 | label={messages.zip.label} 53 | name="zip" 54 | type="text" 55 | required 56 | /> 57 | <Input 58 | defaultValue={garden?.city} 59 | label={messages.city.label} 60 | name="city" 61 | type="text" 62 | required 63 | /> 64 | <Button className="mt-4" type="submit"> 65 | {messages.actions[garden ? "update" : "create"]} 66 | </Button> 67 | </div> 68 | </Form> 69 | ); 70 | } 71 | 72 | export default island("GardenForm", GardenForm); 73 | -------------------------------------------------------------------------------- /resources/components/Gardens/GardensList.island.tsx: -------------------------------------------------------------------------------- 1 | import ExclamationCircleIcon from "@heroicons/react/24/outline/ExclamationCircleIcon"; 2 | import PencilIcon from "@heroicons/react/24/solid/PencilIcon"; 3 | import TrashIcon from "@heroicons/react/24/solid/TrashIcon"; 4 | import { Form, island, useHydrated, useI18n } from "@microeinhundert/radonis"; 5 | import type Garden from "App/Models/Garden"; 6 | import { useState } from "react"; 7 | 8 | import { useAuthenticatedUser } from "../../hooks/useAuthenticatedUser"; 9 | import Button, { ButtonColor } from "../Button.island"; 10 | import Card from "../Card.island"; 11 | import Fallback from "../Fallback"; 12 | import Grid from "../Grid"; 13 | import IconCircle, { IconCircleColor } from "../IconCircle"; 14 | import NoDataIllustration from "../Illustrations/NoDataIllustration"; 15 | import Modal from "../Modal.island"; 16 | 17 | /* 18 | * Gardens List Item 19 | */ 20 | interface GardensListItemProps { 21 | canEdit: boolean; 22 | garden: Garden; 23 | onDelete: (garden: Garden) => void; 24 | onRollback: (garden: Garden) => void; 25 | } 26 | 27 | function GardensListItem({ canEdit, garden, onDelete, onRollback }: GardensListItemProps) { 28 | const { formatMessage$ } = useI18n(); 29 | const hydrated = useHydrated(); 30 | const [deleteConfirmationModalOpen, setDeleteConfirmationModalOpen] = useState(false); 31 | 32 | const messages = { 33 | actions: { 34 | edit: formatMessage$("gardens.list.actions.edit"), 35 | delete: formatMessage$("gardens.list.actions.delete"), 36 | }, 37 | modals: { 38 | delete: { 39 | title: formatMessage$("gardens.list.modals.delete.title", { name: garden.name }), 40 | description: formatMessage$("gardens.list.modals.delete.description"), 41 | actions: { 42 | cancel: formatMessage$("gardens.list.modals.delete.actions.cancel"), 43 | confirm: formatMessage$("gardens.list.modals.delete.actions.confirm"), 44 | }, 45 | }, 46 | }, 47 | }; 48 | 49 | return ( 50 | <Form 51 | action$="gardens.destroy" 52 | hooks={{ 53 | onMutate: () => { 54 | setDeleteConfirmationModalOpen(false); 55 | onDelete(garden); 56 | return () => onRollback(garden); 57 | }, 58 | onFailure: ({ rollback }) => { 59 | rollback?.(); 60 | }, 61 | }} 62 | id={`delete-garden-${garden.id}`} 63 | method="delete" 64 | params={{ id: garden.id }} 65 | noReload 66 | > 67 | <Card> 68 | <Card.Head 69 | actions={ 70 | canEdit && ( 71 | <> 72 | <Button.Link 73 | color={ButtonColor.Emerald} 74 | icon={PencilIcon} 75 | params={{ id: garden.id }} 76 | title={messages.actions.edit} 77 | to$="gardens.edit" 78 | round 79 | small 80 | /> 81 | <Button 82 | color={ButtonColor.WhiteDanger} 83 | icon={TrashIcon} 84 | title={messages.actions.delete} 85 | type={hydrated ? "button" : "submit"} 86 | round 87 | small 88 | onClick={() => { 89 | setDeleteConfirmationModalOpen(true); 90 | }} 91 | /> 92 | </> 93 | ) 94 | } 95 | > 96 | {garden.name} 97 | </Card.Head> 98 | <Card.Body> 99 | <p>{garden.city}</p> 100 | <p>{garden.zip}</p> 101 | </Card.Body> 102 | </Card> 103 | <Modal 104 | actions={ 105 | <> 106 | <Button 107 | color={ButtonColor.White} 108 | round 109 | onClick={() => setDeleteConfirmationModalOpen(false)} 110 | > 111 | {messages.modals.delete.actions.cancel} 112 | </Button> 113 | <Button color={ButtonColor.Red} form={`delete-garden-${garden.id}`} type="submit" round> 114 | {messages.modals.delete.actions.confirm} 115 | </Button> 116 | </> 117 | } 118 | open={deleteConfirmationModalOpen} 119 | onClose={() => setDeleteConfirmationModalOpen(false)} 120 | > 121 | <Modal.Aside> 122 | <IconCircle color={IconCircleColor.Red} icon={ExclamationCircleIcon} /> 123 | </Modal.Aside> 124 | <Modal.Body> 125 | <Modal.Title>{messages.modals.delete.title}</Modal.Title> 126 | <Modal.Description>{messages.modals.delete.description}</Modal.Description> 127 | </Modal.Body> 128 | </Modal> 129 | </Form> 130 | ); 131 | } 132 | 133 | /* 134 | * Gardens List 135 | */ 136 | interface GardensListProps { 137 | gardens: Garden[]; 138 | } 139 | 140 | function GardensList({ gardens }: GardensListProps) { 141 | const { formatMessage$ } = useI18n(); 142 | const user = useAuthenticatedUser(); 143 | const [gardensListItems, setGardensListItems] = useState<Garden[]>(gardens); 144 | 145 | const messages = { 146 | noData: { 147 | headline: formatMessage$("gardens.list.noData.headline"), 148 | text: formatMessage$("gardens.list.noData.text"), 149 | }, 150 | }; 151 | 152 | return ( 153 | <> 154 | {gardensListItems.length ? ( 155 | <> 156 | <Grid> 157 | {gardensListItems.map((garden) => ( 158 | <GardensListItem 159 | key={garden.id} 160 | canEdit={user?.id === garden.userId} 161 | garden={garden} 162 | onDelete={(garden) => { 163 | setGardensListItems((gardens) => gardens.filter(({ id }) => id !== garden.id)); 164 | }} 165 | onRollback={(garden) => { 166 | setGardensListItems((gardens) => [garden, ...gardens]); 167 | }} 168 | /> 169 | ))} 170 | </Grid> 171 | </> 172 | ) : ( 173 | <Fallback {...messages.noData} icon={NoDataIllustration} /> 174 | )} 175 | </> 176 | ); 177 | } 178 | 179 | export default island("GardensList", GardensList); 180 | -------------------------------------------------------------------------------- /resources/components/Grid.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { forwardRef } from "react"; 3 | 4 | import { clsx } from "../utils/string"; 5 | 6 | /* 7 | * Grid 8 | */ 9 | export enum GridType { 10 | OneColumn = "oneColumn", 11 | TwoColumns = "twoColumns", 12 | ThreeColumns = "threeColumns", 13 | FourColumns = "fourColumns", 14 | } 15 | 16 | interface GridProps { 17 | children: ReactNode; 18 | type?: GridType; 19 | } 20 | 21 | const Grid = forwardRef<HTMLDivElement, GridProps>(function ( 22 | { children, type = GridType.ThreeColumns }, 23 | ref 24 | ) { 25 | return ( 26 | <div 27 | ref={ref} 28 | className={clsx( 29 | "grid gap-8", 30 | { 31 | [GridType.OneColumn]: "grid-cols-1", 32 | [GridType.TwoColumns]: "grid-cols-1 sm:grid-cols-2", 33 | [GridType.ThreeColumns]: "grid-cols-1 sm:grid-cols-2 md:grid-cols-3", 34 | [GridType.FourColumns]: "grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4", 35 | }[type] 36 | )} 37 | > 38 | {children} 39 | </div> 40 | ); 41 | }); 42 | 43 | export default Grid; 44 | -------------------------------------------------------------------------------- /resources/components/Header.island.tsx: -------------------------------------------------------------------------------- 1 | import { island } from "@microeinhundert/radonis"; 2 | import type { ReactNode } from "react"; 3 | 4 | /* 5 | * Header 6 | */ 7 | interface HeaderProps { 8 | title: string; 9 | actions?: ReactNode; 10 | } 11 | 12 | function Header({ title, actions }: HeaderProps) { 13 | return ( 14 | <header className="mb-12 flex flex-col items-start justify-between gap-8 sm:flex-row sm:items-center"> 15 | <div className="flex-1"> 16 | <h1 className="truncate text-4xl font-bold text-gray-900">{title}</h1> 17 | </div> 18 | {actions && <div className="flex items-center gap-4">{actions}</div>} 19 | </header> 20 | ); 21 | } 22 | 23 | export default island("Header", Header); 24 | -------------------------------------------------------------------------------- /resources/components/IconCircle.tsx: -------------------------------------------------------------------------------- 1 | import { clsx } from "../utils/string"; 2 | 3 | /* 4 | * Icon Circle 5 | */ 6 | export enum IconCircleColor { 7 | Emerald = "emerald", 8 | Red = "red", 9 | Indigo = "indigo", 10 | } 11 | 12 | interface IconCircleProps extends HTMLProps<"div"> { 13 | icon: IconComponent; 14 | color?: IconCircleColor; 15 | large?: boolean; 16 | } 17 | 18 | function IconCircle({ 19 | icon: Icon, 20 | color = IconCircleColor.Emerald, 21 | large, 22 | className, 23 | ...restProps 24 | }: IconCircleProps) { 25 | return ( 26 | <div 27 | className={clsx( 28 | "inline-flex aspect-square items-center justify-center rounded-full", 29 | { 30 | [IconCircleColor.Emerald]: "bg-emerald-100 text-emerald-600", 31 | [IconCircleColor.Red]: "bg-red-100 text-red-600", 32 | [IconCircleColor.Indigo]: "bg-indigo-100 text-indigo-600", 33 | }[color], 34 | large ? "w-16" : "w-10", 35 | className 36 | )} 37 | {...restProps} 38 | > 39 | <Icon className={clsx("aspect-square", large ? "w-8" : "w-5")} /> 40 | </div> 41 | ); 42 | } 43 | 44 | export default IconCircle; 45 | -------------------------------------------------------------------------------- /resources/components/Illustrations/NoDataIllustration.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * No Data Illustration 3 | */ 4 | interface NoDataIllustrationProps { 5 | className?: string; 6 | } 7 | 8 | function NoDataIllustration(props: NoDataIllustrationProps) { 9 | return ( 10 | <svg {...props} fill="currentColor" viewBox="0 0 800 800"> 11 | <path d="M265.21 511.69c-5.08-.64-10.04-1.87-14.93-3.42-4.88-1.57-9.66-3.58-14.26-6.12-2.31-1.24-4.56-2.65-6.74-4.24-2.18-1.57-4.25-3.37-6.16-5.42-1.9-2.05-3.59-4.45-4.8-7.16-.58-1.37-1.06-2.81-1.34-4.3-.27-1.49-.33-3.02-.23-4.51.11-1.66 1.55-2.91 3.2-2.79 1.47.1 2.62 1.24 2.78 2.65l.02.14c.46 4.02 2.75 7.51 5.94 10.57 3.19 3.06 7.12 5.64 11.25 7.99 4.16 2.31 8.53 4.39 12.97 6.46 4.43 2.09 8.97 4.04 13.46 6.31l.01.01c1 .51 1.4 1.73.89 2.73-.4.79-1.24 1.21-2.06 1.1zM345.65 273.22c7.34-5.42 15.97-9.05 24.83-11.32 8.9-2.25 18.09-3.09 27.2-2.86 9.12.23 18.19 1.55 26.98 4 4.39 1.23 8.7 2.8 12.89 4.69 4.16 1.93 8.25 4.2 11.85 7.27 1.06.9 1.18 2.49.28 3.55-.76.89-1.99 1.12-3 .64l-.08-.04c-1.86-.89-3.78-1.69-5.73-2.43-1.98-.69-3.94-1.4-5.98-1.95-4.03-1.21-8.16-2.14-12.31-2.94-8.31-1.59-16.75-2.51-25.19-2.77-8.44-.24-16.88.13-25.21 1.34-8.34 1.22-16.52 3.31-24.6 6.35l-.03.01c-1.04.39-2.21-.14-2.6-1.18-.31-.89-.01-1.84.7-2.36z" /> 12 | <path d="M303.76 307.99c-15.26-13.91-28.5-30.07-39.26-47.77-10.74-17.71-19.11-36.92-24.59-56.92-2.78-9.99-4.87-20.16-6.29-30.42-1.4-10.26-2.05-20.61-2.14-30.94l.04-7.75.32-7.73c.06-2.58.35-5.14.53-7.71.2-2.57.39-5.14.75-7.69l.96-7.66c.38-2.54.82-5.08 1.23-7.61l.62-3.8.77-3.77 1.58-7.54c.33-2.11 2.3-3.56 4.42-3.23.43.07.83.2 1.2.39l.21.11c2.32 1.22 4.4 2.4 6.52 3.68 2.1 1.28 4.17 2.59 6.21 3.94 4.08 2.7 8.1 5.5 12 8.45 7.79 5.9 15.26 12.24 22.31 19.03 7.08 6.76 13.69 14 19.92 21.57 6.17 7.61 11.94 15.56 17.19 23.84 5.15 8.35 9.84 16.99 13.96 25.89 1.91 4.52 3.96 8.98 5.59 13.61l1.29 3.44.64 1.72.55 1.75 2.2 7.01c2.66 9.43 4.99 18.96 6.32 28.65 1.5 9.66 2.1 19.42 2.3 29.15.04 1.66-1.28 3.03-2.94 3.06-1.57.03-2.88-1.15-3.05-2.68l-.01-.14-.72-7.07c-.24-2.35-.4-4.72-.8-7.05l-1.04-7.01-.26-1.75-.35-1.74-.7-3.47c-1.68-9.29-4.22-18.38-7.1-27.33l-2.36-6.65-.59-1.66-.66-1.63-1.31-3.27c-1.66-4.4-3.73-8.63-5.65-12.91-4.14-8.43-8.68-16.67-13.74-24.59-5.11-7.89-10.63-15.51-16.6-22.79-5.91-7.32-12.29-14.26-18.98-20.9-6.7-6.62-13.75-12.9-21.11-18.8-3.65-2.99-7.41-5.83-11.23-8.58-1.91-1.38-3.85-2.71-5.8-4.01-1.92-1.31-3.95-2.61-5.82-3.73l5.82-2.73c-3.32 19.72-5.14 39.65-4.53 59.51.34 9.92 1.19 19.81 2.61 29.61 1.51 9.79 3.58 19.48 6.24 29l1 3.57 1.13 3.53 1.13 3.53c.39 1.17.82 2.33 1.23 3.5.82 2.33 1.63 4.67 2.56 6.96.89 2.3 1.78 4.61 2.79 6.87 3.89 9.09 8.29 17.97 13.28 26.52 4.93 8.59 10.47 16.83 16.44 24.75 5.93 7.96 12.45 15.48 19.32 22.66l.03.03c1.53 1.6 1.47 4.13-.13 5.66-1.52 1.44-3.91 1.44-5.45.04zM481.93 303.49c5.76-5.79 11.23-11.88 16.31-18.27 5.09-6.39 9.83-13.05 14.21-19.93 4.38-6.88 8.4-13.99 11.97-21.3 3.59-7.31 6.79-14.8 9.62-22.41l.02-.04c.96-2.59 3.84-3.91 6.43-2.95s3.91 3.84 2.95 6.43c-.01.04-.04.1-.05.14-3.19 7.87-6.76 15.57-10.73 23.06-3.95 7.5-8.35 14.75-13.1 21.75s-9.87 13.74-15.33 20.19c-5.45 6.46-11.29 12.57-17.43 18.38-1.4 1.33-3.62 1.27-4.95-.13-1.3-1.38-1.27-3.53.05-4.87l.03-.05z" /> 13 | <path d="M487.51 155.81c.52 1.09 1.01 2.19 1.5 3.28.25.55.47 1.1.73 1.63l.81 1.6c.53 1.07 1.03 2.14 1.59 3.19.59 1.04 1.16 2.07 1.71 3.12 2.37 4.1 4.82 8.12 7.64 11.9 2.73 3.83 5.68 7.5 8.82 11.01 3.12 3.52 6.35 6.96 9.8 10.19.84.83 1.73 1.61 2.61 2.4.88.79 1.75 1.59 2.66 2.35 1.77 1.57 3.59 3.09 5.43 4.59 3.71 2.95 7.49 5.83 11.39 8.56 3.89 2.73 7.88 5.36 11.93 7.87 2.03 1.26 4.08 2.47 6.14 3.67l3.1 1.76c1.02.58 2.1 1.17 3.05 1.66l-5.56 3.85c-.42-12.16-1.3-24.41-2.63-36.52-.65-6.05-1.48-12.09-2.55-18.02-.54-2.96-1.12-5.91-1.85-8.76-.72-2.83-1.57-5.66-2.65-7.99-.61-1.17-.94-1.73-1.96-2.8-.43-.49-.95-.94-1.42-1.42l-1.56-1.35c-2.15-1.77-4.55-3.37-7.08-4.8-5.05-2.88-10.51-5.2-16.09-7.1-2.79-.95-5.62-1.79-8.46-2.53-2.85-.71-5.71-1.33-8.57-1.83-2.86-.47-5.71-.84-8.51-1.01-2.79-.15-5.55-.12-8.04.24-1.22.2-2.43.43-3.4.81-.54.15-.92.4-1.37.58l-.53.34c-.09.06-.17.09-.28.17l-.32.26c-1.72 1.34-3.62 3.49-5.33 5.7-3.48 4.49-6.57 9.62-9.45 14.82-5.78 10.45-10.65 21.55-14.93 32.85-2.14 5.66-4.09 11.38-5.89 17.16-1.83 5.77-3.45 11.61-4.89 17.49-1.46 5.88-2.8 11.79-3.85 17.76-.29 1.49-.5 2.99-.76 4.48-.26 1.49-.48 2.99-.68 4.49-.47 3-.82 6.01-1.19 9.02l-.01.05c-.2 1.65-1.7 2.82-3.34 2.61-1.56-.19-2.69-1.56-2.63-3.1 1.01-24.93 6.1-49.61 14.16-73.25 1.99-5.92 4.24-11.77 6.67-17.54 2.44-5.77 5.07-11.49 8.03-17.07 3-5.58 6.17-11.08 10.15-16.37 2.03-2.65 4.12-5.25 7.19-7.76l.58-.47c.2-.16.46-.31.68-.46.47-.3.94-.62 1.42-.89.95-.45 1.92-.95 2.86-1.24 1.91-.71 3.74-1.04 5.55-1.34 3.6-.53 7.02-.45 10.36-.19s6.57.79 9.75 1.48c6.36 1.35 12.46 3.37 18.38 5.77 5.93 2.41 11.65 5.31 17.15 8.74 2.74 1.74 5.41 3.65 7.96 5.84l1.87 1.73c.6.63 1.22 1.22 1.79 1.89.58.7 1.13 1.27 1.75 2.23l.44.67c.14.22.23.42.35.63l.65 1.26c1.47 3.28 2.3 6.39 3.07 9.51.75 3.12 1.32 6.21 1.84 9.3 1.04 6.18 1.77 12.35 2.48 18.51.68 6.16 1.23 12.32 1.73 18.48.52 6.17.94 12.31 1.33 18.52.22 2.12-1.32 4.01-3.44 4.22-.6.06-1.19-.02-1.72-.22l-.4-.15c-2.51-.94-4.75-1.89-7.05-2.96-2.29-1.04-4.52-2.18-6.75-3.34-4.43-2.35-8.75-4.91-12.95-7.67-4.19-2.77-8.28-5.7-12.2-8.86-1.95-1.59-3.89-3.2-5.78-4.87-.96-.82-1.87-1.7-2.79-2.56-.92-.86-1.84-1.72-2.73-2.63-7.22-7.06-13.69-15.02-18.81-23.81-5.13-8.77-8.91-18.39-10.57-28.33-.18-1.1.56-2.13 1.65-2.32.91-.15 1.77.33 2.15 1.12l.07.17zM258.6 176.65c-1.31-4.35-2.17-8.79-2.79-13.25-.56-4.46-.86-8.94-1-13.41-.11-4.47-.04-8.95.17-13.41.22-4.49.55-8.87 1.13-13.47.3-2.39 2.48-4.08 4.87-3.78.83.1 1.58.44 2.19.93l.3.25c5.22 4.36 10.3 8.92 15.08 13.77 1.2 1.21 2.4 2.42 3.57 3.66l3.43 3.79 1.72 1.9 1.65 1.96 3.29 3.91c8.58 10.61 16.15 22.05 22.5 34.12 6.39 12.05 11.59 24.72 15.52 37.75 1.94 6.53 3.59 13.13 4.91 19.81.63 3.35 1.16 6.71 1.61 10.1.22 1.69.42 3.39.58 5.1.15 1.73.27 3.38.34 5.24l.01.22c.05 1.42-.82 2.67-2.07 3.18-1.74.66-3.57 1.43-5.33 2.21-1.77.78-3.52 1.61-5.23 2.49-1.71.87-3.39 1.81-4.97 2.82-1.57 1.01-3.11 2.11-4.27 3.32l-.05.05c-.76.8-2.03.83-2.83.07-.74-.7-.82-1.84-.23-2.64 1.37-1.85 2.95-3.26 4.55-4.62 1.61-1.35 3.27-2.57 4.97-3.72 1.69-1.16 3.41-2.26 5.16-3.31 1.76-1.06 3.48-2.06 5.35-3.05l-2.06 3.39c-.17-1.47-.42-3.13-.68-4.71-.25-1.6-.54-3.2-.85-4.8-.62-3.2-1.36-6.38-2.13-9.55-1.54-6.34-3.39-12.62-5.52-18.79-4.21-12.36-9.43-24.36-15.64-35.81-6.16-11.48-13.45-22.32-21.45-32.59l-3.09-3.78-1.55-1.89-1.61-1.83-3.23-3.67-3.34-3.56c-2.18-2.42-4.54-4.67-6.84-6.98-1.13-1.17-2.37-2.24-3.56-3.36l-3.58-3.35 7.36-2.6c-.91 4.04-1.65 8.36-2.26 12.58-.61 4.25-1.08 8.53-1.39 12.81-.27 4.28-.39 8.57-.27 12.85.18 4.26.6 8.51 1.42 12.66l.01.05c.22 1.08-.49 2.14-1.57 2.35-1.02.17-2.01-.44-2.3-1.41z" /> 14 | <path d="M494.2 174.55c-4.71 5.76-8.73 12.1-12.28 18.73-3.45 6.7-6.42 13.65-9.07 20.75-2.58 7.12-4.81 14.38-6.75 21.72-.97 3.67-1.86 7.36-2.66 11.07-.81 3.68-1.55 7.47-2.13 11.07l-2.75-4.91c.8.39 1.41.72 2.07 1.1.65.36 1.29.74 1.93 1.12 1.27.76 2.5 1.56 3.72 2.37 2.43 1.64 4.75 3.42 6.98 5.31 2.24 1.87 4.34 3.93 6.31 6.08 1.99 2.14 3.74 4.51 5.37 6.92.62.92.38 2.16-.54 2.78-.83.56-1.93.41-2.59-.3l-.07-.08c-1.82-1.97-3.69-3.86-5.79-5.52-2.08-1.68-4.24-3.24-6.53-4.63-2.27-1.4-4.61-2.69-7.02-3.84-1.2-.57-2.41-1.12-3.63-1.62-.61-.25-1.22-.5-1.83-.72-.59-.22-1.25-.46-1.74-.61l-.28-.12c-1.87-.85-2.85-2.86-2.48-4.79.77-3.99 1.64-7.74 2.61-11.56.95-3.8 1.99-7.57 3.11-11.32 2.24-7.5 4.79-14.91 7.69-22.19 2.97-7.26 6.3-14.39 10.14-21.29 3.95-6.84 8.42-13.42 13.68-19.44 1.09-1.25 2.99-1.37 4.23-.28 1.22 1.07 1.37 2.9.35 4.15l-.05.05zM529.32 215.14c-2.06 3.81-4.14 7.56-6.27 11.31-2.14 3.74-4.31 7.47-6.69 11.13-.6.93-1.84 1.2-2.77.59-.84-.54-1.14-1.61-.74-2.5l.01-.02c1.71-3.83 3.26-7.8 4.77-11.77 1.51-3.97 2.97-7.99 4.38-11.98v-.01c.74-2.09 3.03-3.19 5.12-2.45s3.19 3.03 2.45 5.12c-.07.2-.17.41-.26.58zM351.08 339.86c0 8.9-5.92 15.6-13.67 16.12-7.54.5-14.84-6.43-14.35-16.1.48-9.39 6.24-15.53 13.79-15.53 7.55-.01 14.23 6.61 14.23 15.51zM467.61 339.12c0 8.79-6.09 15.4-14.06 15.91-7.76.5-15.27-6.34-14.76-15.9.49-9.27 6.42-15.33 14.19-15.33s14.63 6.53 14.63 15.32zM398.76 369.83c-.02-.02 0 .19 0 .31l.05.45c.04.31.08.63.14.95.11.64.25 1.29.41 1.94.33 1.3.75 2.58 1.26 3.83 1.03 2.5 2.43 4.83 4.23 6.86.91 1 1.91 1.92 3.01 2.76 1.1.83 2.31 1.54 3.57 2.18 2.56 1.21 5.43 2 8.41 2.37h.02c.83.1 1.41.85 1.31 1.68-.09.72-.67 1.26-1.36 1.32-3.26.3-6.65.14-10.02-.64-1.67-.43-3.34-.97-4.95-1.71-1.6-.75-3.15-1.64-4.61-2.68-2.9-2.11-5.41-4.75-7.46-7.7-1.02-1.47-1.94-3.02-2.75-4.64-.4-.81-.78-1.63-1.13-2.49-.18-.43-.34-.86-.5-1.31l-.24-.69c-.08-.26-.14-.44-.24-.84-.54-2.99 1.45-5.85 4.44-6.39 3.01-.54 5.87 1.45 6.41 4.44z" /> 15 | <path d="M371.24 391.56l1.89-.75c.62-.26 1.26-.43 1.84-.75l1.74-.9c.28-.15.58-.28.85-.45l.79-.53c2.17-1.3 3.88-3.05 5.34-4.87 1.44-1.84 2.49-3.93 3.19-6.14l.46-1.67c.1-.57.29-1.12.42-1.68.15-.56.31-1.12.09-1.65.09-.28.17-.54.27-.81l.15-.38c.05-.13.11-.25.17-.26l7-1.29c.92-.17 1.81.44 1.98 1.37.01.03.01.07.02.1l.1.8c.02.24.05.47.05.69.01.44.03.89-.02 1.31-.03.87-.18 1.71-.36 2.54-.18.83-.46 1.64-.75 2.43-.34.78-.69 1.56-1.06 2.32-.42.75-.82 1.5-1.25 2.22l-1.44 2.11c-.54.66-1.11 1.3-1.66 1.94-.56.64-1.23 1.18-1.83 1.76-.6.6-1.29 1.08-1.98 1.57-.69.48-1.35.99-2.1 1.35l-2.19 1.15c-.75.32-1.53.58-2.28.86l-1.13.41c-.38.12-.77.19-1.16.28l-2.31.49c-1.54.24-3.09.28-4.6.39-1.1.08-2.06-.76-2.13-1.86-.06-.89.47-1.68 1.25-1.99l.25-.11z" /> 16 | <path d="M376.22 354.64c-4.42 3.69 9.44 20.01 17.09 20.01s21.36-13.27 17.77-18.22c-3.6-4.93-30.81-5.16-34.86-1.79zM521.42 593.06c-12.78 4.74-25.84 7.82-39.07 10.38-13.23 2.46-26.58 4.17-39.98 5.3-13.4 1.12-26.83 1.7-40.28 1.9-13.44.1-26.89-.26-40.32-1.12-13.42-.93-26.82-2.32-40.15-4.31-13.33-2.04-26.58-4.67-39.69-8.11-3.28-.86-6.54-1.79-9.79-2.76-3.25-1-6.48-2.07-9.69-3.21-6.44-2.25-12.75-4.94-18.92-8.06-6.12-3.21-12.14-6.85-17.47-11.74-2.66-2.44-5.11-5.24-7.06-8.54-.94-1.67-1.74-3.47-2.28-5.38-.53-1.91-.78-3.94-.68-5.95.08-1.66 1.49-2.93 3.15-2.85 1.52.08 2.71 1.26 2.84 2.73l.01.12c.47 5.33 4.44 10.03 9.24 13.78 4.83 3.78 10.47 6.85 16.29 9.52 11.69 5.37 24.23 9.27 36.89 12.61 12.7 3.28 25.65 5.8 38.68 7.76 13.03 2.04 26.16 3.5 39.34 4.51 26.34 2.12 52.87 2.35 79.27.81 13.2-.82 26.36-2.16 39.46-3.94 6.54-.95 13.09-1.91 19.58-3.12 6.48-1.18 13.03-2.5 19.31-4.13l.1-.03c1.07-.28 2.16.36 2.44 1.43.28 1.02-.28 2.05-1.22 2.4zM579.72 478.77c.09 1.92-.21 3.83-.78 5.59-.57 1.76-1.38 3.37-2.31 4.85-.93 1.49-1.98 2.84-3.12 4.07-1.12 1.26-2.32 2.39-3.55 3.48-4.94 4.29-10.39 7.61-15.96 10.54-11.17 5.82-22.95 9.97-34.83 13.48-11.91 3.44-23.99 6.12-36.15 8.26-12.17 2.09-24.41 3.67-36.69 4.78-12.28 1.06-24.6 1.66-36.92 1.84-12.32.15-24.77-.3-37.03-1.49-12.29-1.15-24.49-2.92-36.58-5.18-6.05-1.11-12.06-2.4-18.05-3.8-6-1.39-11.92-2.94-17.86-4.73-1.32-.4-2.07-1.79-1.68-3.12.37-1.23 1.6-1.96 2.83-1.74l.05.01c11.9 2.11 23.95 3.89 35.95 5.45 12.02 1.54 24.06 2.79 36.1 3.62 6.02.37 12.04.73 18.06.89 3.01.15 6.01.09 9.02.13 3 .03 6.03.01 9.05-.03 12.09-.16 24.17-.74 36.21-1.76 12.04-.97 24.05-2.38 35.95-4.3 11.91-1.89 23.74-4.28 35.36-7.36 11.61-3.05 23.04-6.83 33.78-11.81 5.33-2.53 10.49-5.42 14.92-8.97 1.09-.89 2.16-1.81 3.11-2.8.96-.98 1.86-1.99 2.61-3.06 1.52-2.12 2.5-4.42 2.51-6.69v-.02c.01-1.66 1.36-2.99 3.02-2.98 1.6.01 2.9 1.28 2.98 2.85z" /> 17 | <path d="M541.75 329.97c5.43 1.61 10.8 3.51 16.05 5.84 5.22 2.38 10.39 5.12 15.1 8.89 2.34 1.9 4.58 4.06 6.49 6.67 1.89 2.6 3.44 5.74 4 9.24.3 1.74.31 3.53.13 5.25-.17 1.74-.58 3.44-1.17 5.03-1.14 3.23-2.94 6-4.93 8.47-4.08 4.85-8.95 8.49-13.94 11.62-5.01 3.13-10.26 5.67-15.58 7.92-10.64 4.5-21.61 7.79-32.67 10.45-44.31 10.51-89.97 12.06-134.88 9.53-22.49-1.23-44.93-3.48-67.23-7.07-11.15-1.81-22.25-4-33.27-6.69-11.01-2.74-21.96-5.96-32.62-10.42-5.32-2.25-10.58-4.81-15.61-8.04-2.51-1.62-4.96-3.43-7.27-5.53-2.31-2.1-4.48-4.53-6.21-7.47-1.73-2.91-2.93-6.46-3-10.12-.11-3.6.79-7.12 2.24-10.15 1.45-3.05 3.38-5.68 5.49-7.99 2.11-2.31 4.42-4.33 6.8-6.16 4.78-3.65 9.88-6.57 15.08-9.14 5.21-2.56 10.54-4.74 15.92-6.69 10.78-3.87 21.79-6.8 32.89-9.16 2.43-.52 4.82 1.03 5.34 3.46.52 2.43-1.03 4.82-3.46 5.34h-.01c-10.78 2.3-21.43 5.13-31.71 8.83-5.13 1.85-10.17 3.92-15 6.29-4.82 2.38-9.46 5.06-13.58 8.21-4.08 3.13-7.72 6.81-9.63 10.85-.95 2-1.45 4.07-1.38 6.07.05 1.94.67 3.88 1.8 5.76 1.12 1.88 2.69 3.68 4.53 5.33 1.83 1.65 3.9 3.17 6.09 4.57 4.4 2.79 9.24 5.13 14.23 7.21 10.01 4.13 20.6 7.19 31.3 9.79 10.72 2.57 21.6 4.61 32.55 6.28 21.91 3.35 44.06 5.3 66.25 6.42 11.09.54 22.22.88 33.28.89 11.08.01 22.15-.35 33.19-1.04 11.04-.67 22.06-1.72 33-3.21 10.94-1.49 21.82-3.35 32.53-5.84 5.35-1.24 10.66-2.64 15.89-4.22 5.23-1.57 10.4-3.31 15.42-5.33 5.03-2 9.89-4.31 14.48-6.92 4.56-2.63 8.84-5.69 12.09-9.32 1.58-1.82 2.97-3.74 3.79-5.77.44-1.01.73-2.04.89-3.08.17-1.05.22-2.09.1-3.11-.23-2.06-1.05-4.14-2.3-6.15-1.27-2-2.95-3.88-4.85-5.63-3.83-3.47-8.36-6.47-13.13-9.08-4.76-2.65-9.77-4.98-14.87-7.15h-.01c-1.02-.43-1.49-1.61-1.06-2.63.42-.91 1.47-1.39 2.43-1.1z" /> 18 | <path d="M503.51 282.74c10.34 1.07 20.65 2.51 30.94 4.15 10.28 1.67 20.51 3.69 30.7 5.99 5.09 1.15 10.17 2.39 15.23 3.71 5.05 1.35 10.09 2.79 15.1 4.34 10.02 3.08 19.93 6.63 29.64 10.82 9.71 4.17 19.23 9.01 28.19 15.08 4.49 3.02 8.8 6.43 12.79 10.33 3.96 3.92 7.66 8.37 10.32 13.73 1.32 2.67 2.36 5.57 2.96 8.6.61 3.03.74 6.21.36 9.32-.76 6.26-3.55 11.84-6.96 16.43-3.44 4.63-7.51 8.47-11.79 11.85-4.29 3.38-8.81 6.32-13.44 8.98-4.63 2.66-9.37 5.04-14.18 7.22-4.8 2.21-9.66 4.22-14.57 6.09-9.82 3.72-19.77 6.96-29.83 9.76-20.09 5.69-40.53 9.87-61.08 13.08-41.13 6.31-82.73 8.97-124.29 8.55-41.51-.54-83.01-3.22-124.21-9-20.59-2.92-41.1-6.62-61.39-11.62-10.14-2.51-20.22-5.34-30.17-8.7-9.95-3.37-19.79-7.21-29.33-11.98-4.77-2.39-9.45-5.01-13.96-8.03-4.51-3.02-8.87-6.4-12.82-10.43-3.92-4.02-7.49-8.78-9.7-14.48-1.09-2.84-1.8-5.9-2-9.01-.2-3.11.13-6.25.87-9.22 1.54-5.96 4.65-11.11 8.22-15.48 3.6-4.38 7.73-8.08 12.02-11.41 8.64-6.61 18.01-11.75 27.57-16.23 19.18-8.88 39.22-15.14 59.44-20.23 20.24-5.07 40.76-8.73 61.36-11.55 2.19-.3 4.21 1.23 4.51 3.42.3 2.17-1.21 4.17-3.37 4.5h-.01c-20.32 3.04-40.49 7.01-60.32 12.15-19.79 5.18-39.34 11.51-57.62 20.19-9.1 4.36-17.9 9.34-25.64 15.38-3.84 3.03-7.42 6.33-10.35 9.97-2.92 3.63-5.17 7.61-6.18 11.7-.99 4.09-.72 8.25.84 12.21 1.54 3.97 4.25 7.7 7.54 11.04 6.64 6.7 15.25 11.93 24.13 16.46 8.94 4.51 18.39 8.24 28 11.53 9.63 3.27 19.44 6.13 29.35 8.64 19.83 5.03 40.03 8.82 60.34 11.84 40.66 5.98 81.82 8.94 122.97 9.74 41.1.65 82.33-1.64 122.93-7.93 20.29-3.15 40.44-7.25 60.14-12.81 9.83-2.84 19.56-5.99 29.01-9.77 9.45-3.76 18.66-8.07 27.21-13.28 4.27-2.6 8.36-5.42 12.11-8.54 3.74-3.11 7.15-6.55 9.78-10.3 2.64-3.74 4.42-7.82 4.8-11.88.4-4.06-.6-8.18-2.64-12.05-2.04-3.87-5.02-7.5-8.45-10.77-3.46-3.27-7.32-6.25-11.41-8.99-8.2-5.48-17.2-10.02-26.44-14.08-9.26-4.03-18.82-7.5-28.51-10.64-4.85-1.56-9.74-3.03-14.65-4.41-4.91-1.41-9.85-2.73-14.81-3.98-9.92-2.51-19.94-4.66-30-6.67-10.05-2.05-20.18-3.74-30.33-5.41h-.01c-2.18-.36-3.66-2.42-3.3-4.6.37-2.08 2.3-3.53 4.39-3.32z" /> 19 | <path d="M289.59 404.41c-3.41-8.32-5.62-17.25-6.35-26.2-.4-4.46-.41-8.97-.25-13.34.16-4.39.52-8.78 1.1-13.16 1.16-8.74 3.22-17.41 6.5-25.68 3.27-8.25 7.8-16.08 13.62-22.84 1.08-1.26 2.98-1.4 4.23-.32 1.22 1.05 1.39 2.89.39 4.14-5.1 6.41-8.97 13.73-11.7 21.47-2.73 7.74-4.35 15.89-5.12 24.12-.39 4.12-.56 8.26-.56 12.41 0 4.17.19 8.22.71 12.25.99 8.05 3.25 15.82 6.61 23.19 1.15 2.51.04 5.48-2.48 6.63-2.51 1.15-5.48.04-6.63-2.48-.02-.05-.05-.11-.07-.16v-.03zM487.09 303.81c5.81 7.05 10.77 14.94 14.31 23.43 1.79 4.23 3.28 8.58 4.51 12.99s2.16 8.87 2.93 13.34c.79 4.48 1.31 8.93 1.76 13.47.41 4.61.5 9.26.22 13.89-.52 9.27-2.49 18.47-5.68 27.21-.95 2.59-3.82 3.93-6.41 2.98-2.59-.95-3.93-3.82-2.98-6.41.01-.04.03-.07.04-.11l.01-.03c3.08-7.75 5.09-15.95 5.79-24.29.37-4.17.42-8.37.16-12.56-.3-4.25-.68-8.6-1.31-12.82-.61-4.24-1.37-8.46-2.39-12.59-1.03-4.13-2.29-8.18-3.83-12.12-3.03-7.9-7.36-15.24-12.58-21.96l-.02-.03c-1.19-1.53-.91-3.73.62-4.91 1.5-1.19 3.65-.94 4.85.52zM578.78 428.48c.46 17.81 1.04 35.64 1.68 53.46.62 17.82 1.24 35.64 1.99 53.45 1.51 35.62 3.12 71.23 5.53 106.76l.95 13.32.12 1.66c.04.59.09 1.02.11 1.81.09 1.44-.03 2.88-.22 4.29-.41 2.83-1.38 5.48-2.6 7.87-2.48 4.79-5.95 8.55-9.58 11.78-3.66 3.23-7.59 5.92-11.61 8.34-8.06 4.81-16.52 8.52-25.09 11.7-8.58 3.18-17.3 5.79-26.09 8.04-17.58 4.48-35.43 7.54-53.34 9.68-17.93 2.12-35.89 3.32-53.94 3.71-18.08.31-36.15-.49-54.12-2.25-17.97-1.79-35.87-4.5-53.54-8.45-8.84-1.97-17.63-4.25-26.33-6.92s-17.32-5.72-25.78-9.42c-4.23-1.86-8.41-3.87-12.51-6.18-4.1-2.29-8.13-4.86-11.99-7.94-1.92-1.56-3.79-3.25-5.56-5.18-1.76-1.93-3.43-4.1-4.78-6.69-1.34-2.57-2.37-5.62-2.49-8.92l.01-1.24.01-.62.02-.43.1-1.66.2-3.32c1.13-17.71 1.81-35.5 2.57-53.29.67-17.79 1.38-35.6 1.93-53.41.58-17.81 1-35.64 1.43-53.47.22-8.91.47-17.83.79-26.74.28-8.91.62-17.83 1.04-26.75v-.01c.05-1.1.99-1.96 2.1-1.9 1.03.05 1.84.88 1.9 1.89 2 35.7 2.16 71.4 2.31 107.12.04 17.86.06 35.73-.32 53.6-.15 8.94-.35 17.88-.66 26.82-.36 8.94-.69 17.89-1.2 26.84l-.17 3.36-.09 1.68-.02.4.01.22c0 .15 0 .29-.01.44.07 1.17.45 2.46 1.16 3.83 1.44 2.77 4.17 5.58 7.24 8.06 3.12 2.49 6.63 4.75 10.32 6.79 3.68 2.06 7.54 3.92 11.48 5.64 7.89 3.45 16.12 6.35 24.47 8.9 8.36 2.55 16.86 4.74 25.43 6.64 17.15 3.8 34.58 6.59 52.1 8.37 17.53 1.8 35.16 2.62 52.76 2.42 17.62-.22 35.31-1.25 52.83-3.17 17.53-1.93 34.95-4.77 51.99-8.95 8.51-2.1 16.92-4.54 25.1-7.49 8.17-2.95 16.14-6.38 23.45-10.66 3.64-2.14 7.11-4.49 10.16-7.15 3.04-2.64 5.7-5.59 7.35-8.74.82-1.57 1.41-3.16 1.64-4.75.11-.79.19-1.58.14-2.38.01-.32-.05-1-.07-1.53l-.1-1.67-.78-13.39c-.96-17.85-1.8-35.7-2.45-53.55-.36-8.92-.63-17.85-.94-26.77l-.69-26.77c-.42-17.85-.77-35.7-.99-53.55-.19-17.85-.33-35.7-.35-53.56 0-1.66 1.34-3 3-3 1.63 0 2.96 1.3 3 2.92l-.01.06z" /> 20 | </svg> 21 | ); 22 | } 23 | 24 | export default NoDataIllustration; 25 | -------------------------------------------------------------------------------- /resources/components/Illustrations/NotFoundIllustration.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Not Found Illustration 3 | */ 4 | interface NotFoundIllustrationProps { 5 | className?: string; 6 | } 7 | 8 | function NotFoundIllustration(props: NotFoundIllustrationProps) { 9 | return ( 10 | <svg {...props} fill="currentColor" viewBox="0 0 800 800"> 11 | <path d="M678.53 358.9c.52-4.94 1.1-9.87 1.72-14.8.6-4.93 1.24-9.85 1.91-14.76 1.33-9.83 2.73-19.65 4.58-29.39l3.64 5.24-506.3-88.98 6.37-4.41-76.69 410.3-4.05-5.84 507.88 87.83-6.94 4.83 7.62-41.23 7.71-41.21 15.64-82.38 15.89-82.34c5.27-27.45 10.53-54.9 17.25-82.09.27-1.07 1.35-1.73 2.43-1.46.99.25 1.63 1.19 1.51 2.18-3.31 27.81-8.07 55.35-12.83 82.89l-14.2 82.64-14.44 82.6-7.33 41.28-7.42 41.26c-.59 3.25-3.68 5.42-6.94 4.83l-507.53-89.81c-2.72-.48-4.54-3.08-4.06-5.8l.01-.04 75.71-410.48c.55-2.98 3.39-4.96 6.37-4.41l505.95 90.95c2.45.44 4.08 2.78 3.64 5.24-1.71 9.76-3.86 19.44-6.09 29.11-1.11 4.83-2.25 9.66-3.42 14.49-1.16 4.83-2.36 9.64-3.61 14.45-.28 1.07-1.38 1.71-2.45 1.43-1.01-.22-1.63-1.15-1.53-2.12z" /> 12 | <path d="M622.85 340.38c5.08-4.25 10.45-8.06 15.86-11.81 5.44-3.7 10.96-7.26 16.6-10.65 2.8-1.73 5.61-3.43 8.45-5.09 2.82-1.69 5.68-3.32 8.55-4.94 5.74-3.22 11.53-6.37 17.5-9.24 1.25-.6 2.76-.07 3.36 1.18.51 1.06.21 2.31-.66 3.03-5.1 4.23-10.38 8.17-15.71 12.04-2.67 1.93-5.34 3.85-8.06 5.71-2.69 1.89-5.42 3.74-8.15 5.56-5.44 3.7-10.98 7.23-16.62 10.63-5.66 3.35-11.37 6.64-17.35 9.48-1.76.83-3.87.08-4.7-1.68-.7-1.48-.28-3.21.93-4.22zM571.29 643.35c3.13 3.38 6.01 6.94 8.8 10.57 2.79 3.64 5.48 7.34 8.08 11.12 2.6 3.78 5.16 7.58 7.6 11.48 2.45 3.89 4.84 7.83 7.02 11.92.52.99.15 2.21-.84 2.74-.79.42-1.74.26-2.36-.33-3.34-3.22-6.47-6.6-9.53-10.02-3.07-3.42-6.03-6.93-8.95-10.47-2.92-3.54-5.74-7.15-8.47-10.82-2.72-3.68-5.36-7.43-7.74-11.37-1.15-1.9-.54-4.37 1.36-5.52 1.66-1.01 3.76-.67 5.03.7z" /> 13 | <path d="M107.65 619.57l63.97-49.39c.88-.68 1.96-.91 2.97-.73l.12.02L569 640.84l-5.82 4 57.62-302.25 3.24 4.68-395.14-67.9 5.77-4-6.62 35.25-6.69 35.23-13.56 70.43-13.75 70.4c-4.56 23.47-9.26 46.91-14.81 70.2-.32 1.34-1.67 2.17-3.02 1.85-1.26-.3-2.07-1.52-1.9-2.78 3.28-23.71 7.41-47.26 11.68-70.79l12.74-70.59 12.92-70.55 6.55-35.26 6.63-35.24v-.01c.51-2.69 3.08-4.46 5.77-3.99l394.79 69.87c2.18.39 3.64 2.47 3.25 4.65l-.01.03-55.65 302.62c-.5 2.71-3.1 4.5-5.8 4h-.01l-393.93-73.34 3.09-.71-66.33 46.17c-.91.63-2.15.41-2.78-.5-.61-.89-.41-2.09.42-2.74zM424.77 116.88c7.25 6.66 14.42 13.39 21.6 20.11 7.19 6.71 14.37 13.44 21.5 20.21l21.4 20.32 21.32 20.4c14.21 13.6 28.31 27.32 42.46 40.99l10.51 10.35c3.45 3.5 6.96 6.95 10.36 10.51 6.86 7.05 13.63 14.2 20.14 21.62.91 1.04.81 2.63-.23 3.54-.94.82-2.31.82-3.25.05-7.63-6.26-14.99-12.79-22.27-19.42-3.66-3.29-7.23-6.68-10.84-10.01l-10.69-10.17c-14.13-13.69-28.31-27.33-42.36-41.09l-21.09-20.64-21.02-20.72c-7-6.91-13.96-13.86-20.91-20.83-6.95-6.96-13.92-13.91-20.81-20.93-1.16-1.18-1.14-3.08.04-4.24 1.15-1.11 2.97-1.12 4.14-.05zM275.55 225.62c4.82-4.74 9.72-9.37 14.59-14.05l14.74-13.88c9.87-9.21 19.79-18.36 29.79-27.42l15.01-13.57c5-4.53 10.01-9.05 15.07-13.51l15.15-13.42c5.09-4.43 10.15-8.89 15.28-13.27 1.68-1.43 4.21-1.23 5.64.45 1.4 1.64 1.24 4.08-.32 5.53-4.95 4.59-9.97 9.09-14.96 13.63l-15.09 13.48c-5.02 4.51-10.09 8.96-15.17 13.39l-15.23 13.33c-10.16 8.88-20.4 17.67-30.69 26.4l-15.5 13.03c-5.21 4.29-10.39 8.62-15.65 12.86-.86.69-2.12.56-2.82-.31-.65-.79-.56-1.95.16-2.67z" /> 14 | <path d="M230.82 272.74c31.28 17.5 62.04 35.83 92.65 54.41l22.91 14.01 22.85 14.12 45.67 28.26 45.59 28.39L506 440.46c15.15 9.54 30.3 19.08 45.4 28.71 15.11 9.61 30.24 19.18 45.28 28.9 1.16.75 1.49 2.3.74 3.46-.73 1.13-2.22 1.47-3.37.8-15.42-9.1-30.76-18.34-46.12-27.54-15.37-9.18-30.69-18.45-46-27.73l-45.89-27.91-45.8-28.05-45.73-28.17-22.85-14.1-22.79-14.21c-30.32-19.04-60.49-38.34-90.15-58.46-.91-.62-1.15-1.87-.53-2.78.58-.88 1.73-1.14 2.63-.64z" /> 15 | <path d="M332.74 603.99C323.2 576.84 314 549.58 305 522.25c-9.02-27.32-17.79-54.72-26.52-82.14-8.74-27.41-17.38-54.85-25.87-82.35-8.56-27.47-17-54.98-25.22-82.56-.39-1.32.36-2.72 1.68-3.11 1.29-.38 2.64.32 3.08 1.58 9.41 27.19 18.62 54.46 27.69 81.76 9.14 27.28 18.14 54.61 27.04 81.97 8.91 27.36 17.78 54.73 26.4 82.18 8.64 27.44 17.07 54.95 25.17 82.57.47 1.59-.44 3.26-2.04 3.72-1.53.46-3.14-.39-3.67-1.88z" /> 16 | <path d="M174.97 554.7c2.11-2.4 4.25-4.47 6.52-6.54 2.25-2.05 4.56-4.04 6.94-5.93 2.39-1.88 4.84-3.7 7.34-5.44 2.51-1.73 5.07-3.38 7.68-4.96 10.44-6.3 21.71-11.32 33.48-14.76 11.76-3.43 24.02-5.31 36.29-5.35 6.13-.04 12.26.37 18.31 1.2 6.05.88 12.04 2.07 17.89 3.75l-3.57 2c1.12-3.94 2.39-7.62 3.79-11.34 1.39-3.71 2.93-7.35 4.53-10.97 3.25-7.21 6.88-14.27 10.94-21.09 8.08-13.66 17.8-26.48 29.33-37.66 2.86-2.82 5.89-5.47 9-8.02 3.12-2.55 6.32-5 9.66-7.27 6.68-4.52 13.78-8.49 21.24-11.64 14.89-6.37 31.17-9.39 47.17-8.94l-3.44 3.34c.04-2.23.15-4.28.33-6.39.16-2.1.5-4.19.67-6.27.54-4.17 1.27-8.3 2.16-12.41 1.81-8.21 4.27-16.3 7.57-24.1 3.3-7.79 7.37-15.34 12.61-22.1 5.21-6.74 11.52-12.77 18.86-17.07.96-.56 2.19-.24 2.75.72.49.84.31 1.88-.39 2.51l-.01.01c-5.94 5.36-11.07 11.4-15.37 18.01-4.3 6.61-7.92 13.67-10.86 21.05-2.94 7.38-5.29 15.03-7.14 22.81-.94 3.89-1.74 7.82-2.45 11.76-.69 3.92-1.31 7.97-1.72 11.79.04 1.75-1.31 3.21-3.04 3.32l-.4.03c-15.1.95-29.87 4.42-43.45 10.65-6.78 3.13-13.3 6.82-19.42 11.13-6.17 4.25-11.95 9.06-17.4 14.27-10.92 10.39-20.37 22.39-28.65 35.17-4.12 6.41-7.98 13.01-11.51 19.8-1.78 3.38-3.46 6.82-5.09 10.29-.81 1.73-1.59 3.48-2.36 5.23-.76 1.73-1.52 3.54-2.18 5.22l-.1.26c-.54 1.4-2.06 2.13-3.47 1.74-11.44-2.36-23.06-3.47-34.64-3.13-11.57.36-23.07 2.18-34.19 5.39-5.55 1.63-11.04 3.52-16.38 5.79-5.35 2.23-10.59 4.78-15.68 7.6-5.09 2.81-10.05 5.9-14.83 9.24-2.4 1.66-4.76 3.38-7.05 5.18-1.15.9-2.28 1.82-3.39 2.76-1.11.92-2.24 1.9-3.23 2.81l-.06.06c-1.01.94-2.59.88-3.53-.13-.9-.98-.89-2.43-.06-3.38zM369.42 361.89c-.55-5.88-.11-11.59.86-17.31.97-5.71 2.64-11.33 4.99-16.73 2.36-5.37 5.45-10.52 9.4-14.99 3.89-4.5 8.69-8.27 13.99-10.84 1.25-.61 2.75-.09 3.36 1.16.51 1.05.22 2.29-.63 3.01l-.06.05c-4.07 3.47-7.73 7.18-10.84 11.37-.4.51-.77 1.05-1.14 1.58-.37.54-.76 1.06-1.12 1.6-.7 1.1-1.41 2.19-2.05 3.34-1.36 2.24-2.55 4.58-3.65 6.98-2.24 4.78-4.01 9.83-5.51 15-1.47 5.15-2.69 10.53-3.6 15.81l-.02.12c-.19 1.09-1.23 1.83-2.32 1.64-.92-.16-1.58-.91-1.66-1.79zM193.36 460.5c1.41-1.62 2.81-3.02 4.31-4.42 1.49-1.39 3.03-2.71 4.61-3.99 3.17-2.55 6.5-4.9 9.99-7.01 3.48-2.13 7.1-4.04 10.86-5.69 3.74-1.68 7.61-3.09 11.56-4.26s7.99-2.03 12.08-2.64c4.09-.61 8.22-.82 12.35-.86 8.25.12 16.48 1.4 24.24 3.98l-3.53 1.78c.8-2.21 1.63-4.19 2.57-6.22.92-2.01 1.9-3.98 2.94-5.93 2.07-3.89 4.33-7.68 6.78-11.34 2.44-3.67 5.06-7.23 7.86-10.64 2.78-3.43 5.74-6.72 8.86-9.85 6.21-6.29 13.13-11.92 20.58-16.68.92-.61 1.89-1.15 2.84-1.72.96-.55 1.89-1.14 2.87-1.66 1.95-1.05 3.9-2.08 5.91-3 3.97-1.91 8.12-3.46 12.3-4.77 1.06-.33 2.18.26 2.52 1.31.29.94-.13 1.92-.97 2.37l-.04.02c-7.49 4-14.69 8.31-21.39 13.33-3.4 2.44-6.58 5.15-9.77 7.83-1.55 1.4-3.12 2.77-4.61 4.24-1.54 1.4-3 2.9-4.48 4.37-2.9 3.01-5.69 6.13-8.38 9.33-2.67 3.22-5.25 6.53-7.69 9.93-2.44 3.41-4.78 6.89-6.98 10.47-1.12 1.78-2.16 3.6-3.21 5.41-1.02 1.8-2.06 3.7-2.93 5.45 0 1.11-.9 2.01-2.01 2.01-.11 0-.22-.01-.33-.03l-1.19-.2c-7.57-1.29-15.13-2.05-22.69-1.87-3.78.08-7.53.42-11.28.9-.94.12-1.86.3-2.8.44l-1.4.23-1.39.29c-1.86.34-3.7.79-5.54 1.24-3.66.98-7.27 2.15-10.83 3.5-3.55 1.37-7.04 2.92-10.45 4.67-3.41 1.75-6.75 3.65-9.98 5.74-3.22 2.07-6.43 4.35-9.3 6.71l-.09.07c-.85.7-2.11.58-2.82-.27-.62-.75-.59-1.85.05-2.57zM209.27 375.44c1.73-1.67 3.44-2.99 5.29-4.31 1.83-1.29 3.74-2.46 5.71-3.54 3.94-2.14 8.13-3.84 12.49-5.03 4.36-1.2 8.88-1.86 13.42-2.01 4.55-.11 9.11.29 13.58 1.17l-3.89 1.81c.71-1.49 1.39-2.74 2.16-4.05.75-1.3 1.53-2.57 2.33-3.83 1.64-2.49 3.39-4.91 5.25-7.25 3.71-4.69 7.92-9.01 12.61-12.84 4.69-3.81 9.89-7.11 15.57-9.47 2.83-1.17 5.77-2.13 8.76-2.82 3-.65 6.06-1.06 9.12-1.12l-2.76 2.95c.03-.71.09-1.19.16-1.76.07-.55.15-1.09.25-1.62.2-1.07.43-2.12.72-3.15.56-2.07 1.3-4.09 2.22-6.04 1.82-3.88 4.34-7.47 7.48-10.4 1.56-1.47 3.25-2.81 5.05-3.93.88-.6 1.83-1.08 2.75-1.6.96-.45 1.89-.95 2.88-1.32 3.89-1.61 7.96-2.6 12.05-3.12 1.1-.14 2.1.64 2.24 1.73.14 1.07-.6 2.05-1.65 2.22-3.77.64-7.43 1.71-10.81 3.28-.86.36-1.65.85-2.48 1.27-.78.5-1.6.94-2.33 1.49-1.52 1.04-2.91 2.24-4.18 3.54-2.56 2.6-4.56 5.71-5.96 9.07-.7 1.68-1.27 3.43-1.67 5.2-.21.89-.36 1.78-.49 2.67-.06.45-.11.89-.15 1.33-.04.42-.06.92-.06 1.21 0 1.51-1.13 2.75-2.59 2.93l-.17.02c-2.67.43-5.3.96-7.85 1.79-2.57.76-5.06 1.76-7.47 2.94-4.82 2.35-9.35 5.37-13.52 8.87-4.19 3.48-8.02 7.45-11.59 11.65-1.79 2.1-3.47 4.3-5.09 6.55-.82 1.12-1.61 2.25-2.38 3.39-.75 1.13-1.53 2.35-2.17 3.4l-.22.35c-.8 1.26-2.3 1.81-3.67 1.46-3.92-1.01-7.95-1.61-12.01-1.75-4.05-.1-8.13.24-12.13 1.1-4 .84-7.92 2.16-11.65 3.92-1.86.88-3.69 1.85-5.44 2.95-1.73 1.07-3.5 2.31-4.93 3.54l-.09.07c-.84.72-2.1.62-2.82-.22-.69-.76-.63-1.96.13-2.69zM187.08 209.36c4.31 4.89 8.38 9.96 12.35 15.09 2.01 2.55 3.93 5.16 5.9 7.74l5.75 7.84c3.77 5.27 7.51 10.57 11.11 15.96 3.61 5.38 7.14 10.83 10.43 16.44.98 1.67.42 3.82-1.25 4.81-1.5.88-3.39.52-4.47-.78-4.17-5-8.09-10.16-11.93-15.39-3.86-5.21-7.57-10.53-11.25-15.86l-5.43-8.07c-1.76-2.73-3.57-5.41-5.28-8.17-3.48-5.48-6.87-11.02-10.01-16.73-.67-1.22-.23-2.75.99-3.42 1.05-.58 2.33-.33 3.09.54z" /> 17 | <path d="M445.06 405.22l-1.5 116.05c-.01 1.1-.92 1.99-2.03 1.97-1.09-.01-1.96-.89-1.97-1.97l-1.5-116.05c-.02-1.93 1.52-3.52 3.45-3.55 1.93-.02 3.52 1.52 3.55 3.45v.1zM402.5 89.83c1.87-1.36 4.17-2.24 6.57-2.55 2.4-.31 4.89-.07 7.22.65 4.7 1.39 8.74 4.61 11.43 8.72.32.53.67 1.04.95 1.59l.83 1.66c.46 1.15.91 2.31 1.2 3.55.61 2.44.83 5.05.51 7.63-.32 2.58-1.14 5.11-2.41 7.39-1.28 2.27-3 4.29-5 5.91-4.02 3.21-9 4.97-14.17 4.88-2.57-.05-5.15-.57-7.53-1.57-2.39-.99-4.55-2.49-6.35-4.3-1.8-1.81-3.22-3.93-4.25-6.16-.51-1.12-.96-2.25-1.27-3.43-.34-1.17-.57-2.38-.7-3.59-.51-4.87 1.07-10.01 4.42-13.31.88-.87 2.3-.86 3.17.02.58.59.77 1.42.57 2.16l-.04.13c-.97 3.55-1.11 6.8-.46 9.94.31 1.55.83 3.12 1.61 4.51.75 1.39 1.7 2.62 2.82 3.61 2.23 1.98 5.12 3.04 8.07 2.96 2.92-.08 5.97-1.23 8.24-3.07 1.15-.91 2.11-2.03 2.82-3.27.73-1.23 1.22-2.6 1.44-4.02.46-2.83-.16-5.99-1.73-8.77-2.99-5.56-9.34-9.74-16.43-8.12l-.09.02c-.95.22-1.9-.38-2.12-1.33-.17-.73.12-1.44.68-1.84z" /> 18 | <path d="M459.51 544.37c0 13.24-7.47 23.21-17.25 23.98-9.51.75-18.73-9.56-18.1-23.96.61-13.98 7.87-23.11 17.4-23.11 9.53-.01 17.95 9.84 17.95 23.09z" /> 19 | <path d="M452.68 572.5c0 6.18-4.59 10.82-10.58 11.18-5.84.35-11.49-4.46-11.11-11.18.37-6.52 4.83-10.78 10.68-10.78 5.84.01 11.01 4.6 11.01 10.78zM427.83 529.65l-13.34-19.21c-1.04-1.49-.82-3.48.42-4.72l.02-.02 25.24-25.14c.78-.78 2.05-.78 2.83.01.73.73.77 1.88.14 2.67l-22.26 27.81.45-4.74 10.77 20.76c.64 1.23.16 2.74-1.07 3.38-1.12.59-2.49.23-3.2-.8z" /> 20 | <path d="M450.83 527.92l11.39-22.9.63 4.69-22.62-26.43c-.72-.84-.62-2.11.22-2.82.78-.67 1.93-.63 2.67.05l25.51 23.66c1.31 1.22 1.55 3.16.66 4.63l-.04.06-13.15 21.94c-.85 1.42-2.7 1.88-4.12 1.03-1.35-.81-1.83-2.52-1.15-3.91zM456.31 535.19l26.9-11.95-2.5 3.89.69-28.84c.03-1.11.95-1.98 2.05-1.96 1.01.02 1.83.8 1.94 1.77l3.3 28.66c.2 1.71-.82 3.28-2.37 3.84l-.13.05-27.65 10.1c-1.56.57-3.28-.23-3.85-1.79-.53-1.49.19-3.13 1.62-3.77zM424.91 541.73l-26.12-10.57-.19-.08c-1.14-.46-1.82-1.58-1.78-2.75l1.03-30.14c.04-1.1.96-1.97 2.07-1.93 1.06.04 1.9.89 1.93 1.94l.97 30.15-1.97-2.82 26.81 8.69c2.1.68 3.26 2.94 2.57 5.04-.68 2.1-2.94 3.26-5.04 2.57-.1-.03-.19-.06-.28-.1zM428.11 552.57l-29.79 9.37 1.55-1.5-8.83 18.68c-.47 1-1.67 1.43-2.67.95-.95-.45-1.38-1.56-1.01-2.52l7.37-19.3c.24-.62.7-1.1 1.26-1.37l.29-.14 28.17-13.47c2.5-1.19 5.49-.14 6.68 2.36 1.19 2.5.14 5.49-2.36 6.68-.21.1-.44.19-.66.26zM457.86 542.7l32.47 12.97.23.09c.52.21.92.57 1.18 1.02l9.73 16.58c.56.95.24 2.18-.71 2.74-.91.53-2.07.26-2.66-.59l-10.93-15.81 1.41 1.11-33.62-9.58c-2.39-.68-3.78-3.17-3.1-5.57.68-2.39 3.17-3.78 5.57-3.1.14.03.29.08.43.14z" /> 21 | <path d="M432.51 560.37l-12.13 15.07.62-4.01 9 30.58c.31 1.06-.29 2.18-1.36 2.49-.97.29-1.99-.2-2.39-1.1l-13.08-29.06c-.57-1.27-.37-2.7.41-3.74l.21-.28 11.62-15.46c1.49-1.99 4.31-2.39 6.3-.9 1.99 1.49 2.39 4.31.9 6.3-.03.02-.07.07-.1.11zM456.26 555.3l12.71 13.73.17.19c.83.9 1.02 2.17.59 3.24l-12.37 30.47c-.42 1.02-1.58 1.52-2.61 1.1-.97-.4-1.46-1.48-1.15-2.46l9.97-31.34.76 3.43-13.73-12.71c-1.62-1.5-1.72-4.03-.22-5.66 1.5-1.62 4.03-1.72 5.66-.22.07.08.15.16.22.23z" /> 22 | </svg> 23 | ); 24 | } 25 | 26 | export default NotFoundIllustration; 27 | -------------------------------------------------------------------------------- /resources/components/Input.island.tsx: -------------------------------------------------------------------------------- 1 | import ExclamationCircleIcon from "@heroicons/react/24/outline/ExclamationCircleIcon"; 2 | import EyeIcon from "@heroicons/react/24/outline/EyeIcon"; 3 | import EyeSlashIcon from "@heroicons/react/24/outline/EyeSlashIcon"; 4 | import { island, useHydrated, useI18n } from "@microeinhundert/radonis"; 5 | import type { HTMLInputTypeAttribute } from "react"; 6 | import { useState } from "react"; 7 | 8 | import { useFormField } from "../hooks/useFormField"; 9 | import { clsx } from "../utils/string"; 10 | 11 | /* 12 | * Input 13 | */ 14 | interface InputProps extends HTMLProps<"input"> { 15 | type?: HTMLInputTypeAttribute; 16 | name: string; 17 | label: string; 18 | description?: string; 19 | } 20 | 21 | const initiallyHiddenTypes: HTMLInputTypeAttribute[] = ["password"]; 22 | 23 | function Input({ type: initialType = "text", className, ...restProps }: InputProps) { 24 | const { formatMessage$ } = useI18n(); 25 | const field = useFormField(restProps); 26 | const isHydrated = useHydrated(); 27 | 28 | const isValueInitiallyHidden = initiallyHiddenTypes.includes(initialType); 29 | const [type, setType] = useState(initialType); 30 | 31 | const messages = { 32 | hideValue: formatMessage$("shared.input.hideValue"), 33 | showValue: formatMessage$("shared.input.showValue"), 34 | }; 35 | 36 | return ( 37 | <div> 38 | <label {...field.getLabelProps()} className="block text-sm font-medium text-gray-700"> 39 | {field.label} 40 | </label> 41 | <div className="relative mt-1 rounded-md shadow-sm"> 42 | <input 43 | {...field.getInputProps({ type, defaultValue: field.value })} 44 | className={clsx( 45 | "block w-full rounded-md pr-10 transition focus:outline-none sm:text-sm", 46 | field.error ? "border-red-300" : "border-gray-300", 47 | field.error ? "focus:border-red-500" : "focus:border-gray-500", 48 | field.error ? "focus:ring-red-500" : "focus:ring-emerald-500", 49 | field.error && "text-red-900 placeholder-red-300", 50 | className 51 | )} 52 | /> 53 | {isHydrated && field.value && isValueInitiallyHidden ? ( 54 | <div className="absolute inset-y-0 right-0 flex items-center pr-3"> 55 | <button 56 | type="button" 57 | onClick={() => setType((type) => (type === initialType ? "text" : initialType))} 58 | > 59 | {type !== initialType ? ( 60 | <EyeSlashIcon aria-hidden="true" className="h-5 w-5" /> 61 | ) : ( 62 | <EyeIcon aria-hidden="true" className="h-5 w-5" /> 63 | )} 64 | <span className="sr-only"> 65 | {messages[type !== initialType ? "hideValue" : "showValue"]} 66 | </span> 67 | </button> 68 | </div> 69 | ) : ( 70 | field.error && ( 71 | <div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3"> 72 | <ExclamationCircleIcon aria-hidden="true" className="h-5 w-5 text-red-500" /> 73 | </div> 74 | ) 75 | )} 76 | </div> 77 | {field.error ? ( 78 | <span {...field.getErrorProps()} className="mt-2 block text-sm text-red-600"> 79 | {field.error} 80 | </span> 81 | ) : ( 82 | field.description && ( 83 | <span {...field.getDescriptionProps()} className="mt-2 block text-sm text-gray-500"> 84 | {field.description} 85 | </span> 86 | ) 87 | )} 88 | </div> 89 | ); 90 | } 91 | 92 | export default island("Input", Input); 93 | -------------------------------------------------------------------------------- /resources/components/Link.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import { clsx } from "../utils/string"; 4 | 5 | /* 6 | * Link 7 | */ 8 | interface LinkProps extends HTMLProps<"a"> { 9 | children?: ReactNode; 10 | } 11 | 12 | function Link({ children, className, ...restProps }: LinkProps) { 13 | return ( 14 | <a 15 | {...restProps} 16 | className={clsx("font-medium text-emerald-600 hover:text-emerald-500", className)} 17 | > 18 | {children} 19 | </a> 20 | ); 21 | } 22 | 23 | export default Link; 24 | -------------------------------------------------------------------------------- /resources/components/Logo.tsx: -------------------------------------------------------------------------------- 1 | /* 2 | * Logo 3 | */ 4 | interface LogoProps { 5 | className?: string; 6 | } 7 | 8 | function Logo(props: LogoProps) { 9 | return ( 10 | <svg aria-hidden="true" fill="none" viewBox="0 0 35 32" {...props}> 11 | <path 12 | className="fill-emerald-500" 13 | d="M15.258 26.865a4.043 4.043 0 01-1.133 2.917A4.006 4.006 0 0111.253 31a3.992 3.992 0 01-2.872-1.218 4.028 4.028 0 01-1.133-2.917c.009-.698.2-1.382.557-1.981.356-.6.863-1.094 1.47-1.433-.024.109.09-.055 0 0l1.86-1.652a8.495 8.495 0 002.304-5.793c0-2.926-1.711-5.901-4.17-7.457.094.055-.036-.094 0 0A3.952 3.952 0 017.8 7.116a3.975 3.975 0 01-.557-1.98 4.042 4.042 0 011.133-2.918A4.006 4.006 0 0111.247 1a3.99 3.99 0 012.872 1.218 4.025 4.025 0 011.133 2.917 8.521 8.521 0 002.347 5.832l.817.8c.326.285.668.551 1.024.798.621.33 1.142.826 1.504 1.431a3.902 3.902 0 01-1.504 5.442c.033-.067-.063.036 0 0a8.968 8.968 0 00-3.024 3.183 9.016 9.016 0 00-1.158 4.244zM19.741 5.123c0 .796.235 1.575.676 2.237a4.01 4.01 0 001.798 1.482 3.99 3.99 0 004.366-.873 4.042 4.042 0 00.869-4.386 4.02 4.02 0 00-1.476-1.806 3.994 3.994 0 00-5.058.501 4.038 4.038 0 00-1.175 2.845zM23.748 22.84c-.792 0-1.567.236-2.226.678a4.021 4.021 0 00-1.476 1.806 4.042 4.042 0 00.869 4.387 3.99 3.99 0 004.366.873A4.01 4.01 0 0027.08 29.1a4.039 4.039 0 00-.5-5.082 4 4 0 00-2.832-1.18zM34 15.994c0-.796-.235-1.574-.675-2.236a4.01 4.01 0 00-1.798-1.483 3.99 3.99 0 00-4.367.873 4.042 4.042 0 00-.869 4.387 4.02 4.02 0 001.476 1.806 3.993 3.993 0 002.226.678 4.003 4.003 0 002.832-1.18A4.04 4.04 0 0034 15.993z" 14 | /> 15 | <path 16 | className="fill-emerald-500" 17 | d="M5.007 11.969c-.793 0-1.567.236-2.226.678a4.021 4.021 0 00-1.476 1.807 4.042 4.042 0 00.869 4.386 4.001 4.001 0 004.366.873 4.011 4.011 0 001.798-1.483 4.038 4.038 0 00-.5-5.08 4.004 4.004 0 00-2.831-1.181z" 18 | /> 19 | </svg> 20 | ); 21 | } 22 | 23 | export default Logo; 24 | -------------------------------------------------------------------------------- /resources/components/Modal.island.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog, Transition } from "@headlessui/react"; 2 | import XMarkIcon from "@heroicons/react/24/outline/XMarkIcon"; 3 | import { island, useI18n } from "@microeinhundert/radonis"; 4 | import type { ReactNode } from "react"; 5 | import { Fragment } from "react"; 6 | 7 | import { clsx } from "../utils/string"; 8 | 9 | /* 10 | * Modal Aside 11 | */ 12 | interface ModalAsideProps { 13 | children: ReactNode; 14 | } 15 | 16 | function ModalAside({ children }: ModalAsideProps) { 17 | return <div className="mb-5 sm:flex-shrink-0">{children}</div>; 18 | } 19 | 20 | /* 21 | * Modal Body 22 | */ 23 | interface ModalBodyProps { 24 | children: ReactNode; 25 | } 26 | 27 | function ModalBody({ children }: ModalBodyProps) { 28 | return <div className="sm:flex-grow">{children}</div>; 29 | } 30 | 31 | /* 32 | * Modal Title 33 | */ 34 | interface ModalTitleProps { 35 | children: ReactNode; 36 | } 37 | 38 | function ModalTitle({ children }: ModalTitleProps) { 39 | return ( 40 | <Dialog.Title as="h3" className="text-lg font-bold text-gray-900"> 41 | {children} 42 | </Dialog.Title> 43 | ); 44 | } 45 | 46 | /* 47 | * Modal Description 48 | */ 49 | interface ModalDescriptionProps { 50 | children: ReactNode; 51 | } 52 | 53 | function ModalDescription({ children }: ModalDescriptionProps) { 54 | return ( 55 | <Dialog.Description className="mt-2 text-center text-sm text-gray-500 sm:text-left"> 56 | {children} 57 | </Dialog.Description> 58 | ); 59 | } 60 | 61 | /* 62 | * Modal Overlay 63 | */ 64 | function ModalOverlay() { 65 | const transition = { 66 | enter: "ease-out duration-200", 67 | enterFrom: "opacity-0", 68 | enterTo: "opacity-100", 69 | leave: "ease-in duration-200", 70 | leaveFrom: "opacity-100", 71 | leaveTo: "opacity-0", 72 | }; 73 | 74 | return ( 75 | <Transition.Child as={Fragment} {...transition}> 76 | <div className="fixed inset-0 bg-gray-600/75 transition-opacity" /> 77 | </Transition.Child> 78 | ); 79 | } 80 | 81 | /* 82 | * Modal Content 83 | */ 84 | interface ModalContentProps { 85 | children: ReactNode; 86 | actions?: ReactNode; 87 | wide?: boolean; 88 | onClose: () => void; 89 | } 90 | 91 | function ModalContent({ children, actions, wide, onClose }: ModalContentProps) { 92 | const { formatMessage$ } = useI18n(); 93 | 94 | const messages = { 95 | close: formatMessage$("shared.modal.close"), 96 | }; 97 | 98 | const transition = { 99 | enter: "ease-out duration-200", 100 | enterFrom: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-90", 101 | enterTo: "opacity-100 translate-y-0 sm:scale-100", 102 | leave: "ease-in duration-200", 103 | leaveFrom: "opacity-100 translate-y-0 sm:scale-100", 104 | leaveTo: "opacity-0 translate-y-4 sm:translate-y-0 sm:scale-90", 105 | }; 106 | 107 | return ( 108 | <Transition.Child as={Fragment} {...transition}> 109 | <Dialog.Panel 110 | className={clsx( 111 | "relative z-10 w-full overflow-hidden rounded-lg bg-white p-6 shadow-xl transition", 112 | wide ? "sm:max-w-5xl" : "sm:max-w-xl" 113 | )} 114 | > 115 | <div className="text-center sm:flex sm:gap-5 sm:text-left">{children}</div> 116 | {actions ? ( 117 | <div className="mt-8 flex justify-center gap-3 sm:justify-end">{actions}</div> 118 | ) : ( 119 | <div className="absolute top-6 right-6"> 120 | <button 121 | className="rounded-md bg-white text-gray-400 hover:text-gray-500 focus:outline-none focus:ring-2 focus:ring-emerald-500 focus:ring-offset-2" 122 | title={messages.close} 123 | type="button" 124 | onClick={onClose} 125 | > 126 | <span className="sr-only">{messages.close}</span> 127 | <XMarkIcon aria-hidden="true" className="h-6 w-6" /> 128 | </button> 129 | </div> 130 | )} 131 | </Dialog.Panel> 132 | </Transition.Child> 133 | ); 134 | } 135 | 136 | /* 137 | * Modal 138 | */ 139 | interface ModalProps extends ModalContentProps { 140 | open: boolean; 141 | } 142 | 143 | function Modal({ open, onClose, ...restProps }: ModalProps) { 144 | return ( 145 | <Transition.Root as={Fragment} show={open}> 146 | <Dialog 147 | as="div" 148 | className="fixed inset-0 z-40 overflow-y-auto" 149 | open={open} 150 | static 151 | onClose={onClose} 152 | > 153 | <div className="flex min-h-screen items-center justify-center px-4 text-center"> 154 | <ModalOverlay /> 155 | <ModalContent {...restProps} onClose={onClose} /> 156 | </div> 157 | </Dialog> 158 | </Transition.Root> 159 | ); 160 | } 161 | 162 | Modal.Aside = ModalAside; 163 | Modal.Body = ModalBody; 164 | Modal.Title = ModalTitle; 165 | Modal.Description = ModalDescription; 166 | 167 | export default island("Modal", Modal); 168 | -------------------------------------------------------------------------------- /resources/components/Search.island.tsx: -------------------------------------------------------------------------------- 1 | import MagnifyingGlassIcon from "@heroicons/react/24/outline/MagnifyingGlassIcon"; 2 | import { island, useI18n } from "@microeinhundert/radonis"; 3 | import { useId } from "react"; 4 | 5 | import { clsx } from "../utils/string"; 6 | 7 | /* 8 | * Search 9 | */ 10 | function Search() { 11 | const { formatMessage$ } = useI18n(); 12 | const id = useId(); 13 | 14 | const messages = { 15 | label: formatMessage$("shared.search.label"), 16 | placeholder: formatMessage$("shared.search.placeholder"), 17 | }; 18 | 19 | return ( 20 | <form action="#" className="flex w-full md:ml-0" method="GET"> 21 | <label className="sr-only" htmlFor={id}> 22 | {messages.label} 23 | </label> 24 | <div className="relative w-full text-gray-400 focus-within:text-gray-600"> 25 | <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center"> 26 | <MagnifyingGlassIcon aria-hidden="true" className="h-5 w-5 flex-shrink-0" /> 27 | </div> 28 | <input 29 | className={clsx( 30 | "h-full w-full border-transparent bg-transparent py-2 pl-8 pr-3 text-sm text-gray-900 placeholder-gray-500", 31 | "focus:border-transparent focus:placeholder-gray-400 focus:outline-none focus:ring-0" 32 | )} 33 | id={id} 34 | name="search-field" 35 | placeholder={messages.placeholder} 36 | type="search" 37 | /> 38 | </div> 39 | </form> 40 | ); 41 | } 42 | 43 | export default island("Search", Search); 44 | -------------------------------------------------------------------------------- /resources/components/Sidebar.tsx: -------------------------------------------------------------------------------- 1 | import { HydrationRoot } from "@microeinhundert/radonis"; 2 | 3 | import { useAuthenticatedUser } from "../hooks/useAuthenticatedUser"; 4 | import { useNavigation } from "../hooks/useNavigation"; 5 | import type { NavigationItem } from "../hooks/useNavigationBuilder"; 6 | import { clsx } from "../utils/string"; 7 | import Logo from "./Logo"; 8 | import SidebarUserInfo from "./SidebarUserInfo.island"; 9 | 10 | /* 11 | * Sidebar Navigation Item 12 | */ 13 | interface SidebarNavigationItemProps extends NavigationItem { 14 | dense?: boolean; 15 | } 16 | 17 | function SidebarNavigationItem({ 18 | name, 19 | href, 20 | icon: Icon, 21 | current, 22 | dense, 23 | }: SidebarNavigationItemProps) { 24 | return ( 25 | <a 26 | className={clsx( 27 | "group flex items-center rounded-md px-2 text-sm font-medium", 28 | dense ? "py-2" : "py-3", 29 | current 30 | ? "bg-gray-100 text-emerald-600" 31 | : "text-gray-600 hover:bg-gray-50 hover:text-gray-900" 32 | )} 33 | href={href} 34 | > 35 | {Icon && ( 36 | <Icon 37 | aria-hidden="true" 38 | className={clsx( 39 | `mr-3 h-6 w-6 flex-shrink-0`, 40 | current ? "text-emerald-600" : "text-gray-400 group-hover:text-gray-500" 41 | )} 42 | /> 43 | )} 44 | {name} 45 | </a> 46 | ); 47 | } 48 | 49 | /* 50 | * Sidebar 51 | */ 52 | function Sidebar() { 53 | const navigation = useNavigation(); 54 | const user = useAuthenticatedUser(); 55 | 56 | return ( 57 | <div className="flex h-full flex-1 flex-col border-r border-gray-200 bg-white"> 58 | <div className="flex flex-1 flex-col overflow-y-auto py-5"> 59 | <div className="flex flex-shrink-0 items-center px-4"> 60 | <Logo className="h-8 w-auto" /> 61 | </div> 62 | <nav className="mt-5 flex-1"> 63 | {!!navigation.primary.length && ( 64 | <div className="space-y-1 px-2"> 65 | {navigation.primary.map((item) => ( 66 | <SidebarNavigationItem key={item.href} {...item} /> 67 | ))} 68 | </div> 69 | )} 70 | {!!navigation.secondary.length && ( 71 | <> 72 | <hr className="my-5 border-t border-gray-200" /> 73 | <div className="space-y-1 px-2"> 74 | {navigation.secondary.map((item) => ( 75 | <SidebarNavigationItem key={item.href} dense={true} {...item} /> 76 | ))} 77 | </div> 78 | </> 79 | )} 80 | </nav> 81 | </div> 82 | <HydrationRoot disabled={!user}> 83 | <SidebarUserInfo /> 84 | </HydrationRoot> 85 | </div> 86 | ); 87 | } 88 | 89 | export default Sidebar; 90 | -------------------------------------------------------------------------------- /resources/components/SidebarUserInfo.island.tsx: -------------------------------------------------------------------------------- 1 | import ArrowLeftOnRectangleIcon from "@heroicons/react/24/outline/ArrowLeftOnRectangleIcon"; 2 | import { island, useI18n } from "@microeinhundert/radonis"; 3 | import { useState } from "react"; 4 | 5 | import { useAuthenticatedUser } from "../hooks/useAuthenticatedUser"; 6 | import { getFirstChar } from "../utils/string"; 7 | import Button, { ButtonColor } from "./Button.island"; 8 | import IconCircle, { IconCircleColor } from "./IconCircle"; 9 | import Modal from "./Modal.island"; 10 | 11 | /* 12 | * Sidebar User Info 13 | */ 14 | function SidebarUserInfo() { 15 | const { formatMessage$ } = useI18n(); 16 | const user = useAuthenticatedUser(); 17 | 18 | const [signOutModalOpen, setSignOutModalOpen] = useState(false); 19 | 20 | const messages = { 21 | signOut: formatMessage$("shared.sidebar.signOut"), 22 | signIn: formatMessage$("shared.sidebar.signIn"), 23 | signUp: formatMessage$("shared.sidebar.signUp"), 24 | modals: { 25 | signOut: { 26 | title: formatMessage$("shared.sidebar.modals.signOut.title"), 27 | description: formatMessage$("shared.sidebar.modals.signOut.description"), 28 | actions: { 29 | cancel: formatMessage$("shared.sidebar.modals.signOut.actions.cancel"), 30 | confirm: formatMessage$("shared.sidebar.modals.signOut.actions.confirm"), 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | return ( 37 | <div className="flex flex-shrink-0 items-center gap-3 border-t border-gray-200 p-4"> 38 | {user ? ( 39 | <> 40 | <div className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-full bg-gray-500"> 41 | <span className="text-uppercase text-xs font-medium leading-none text-white"> 42 | {getFirstChar(user.firstName) + getFirstChar(user.lastName)} 43 | </span> 44 | </div> 45 | <div className="flex-1"> 46 | <p className="text-sm font-medium text-gray-700"> 47 | {user.firstName} {user.lastName} 48 | </p> 49 | <p className="w-32 truncate text-xs font-medium text-gray-500">{user.email}</p> 50 | </div> 51 | <div className="flex-shrink-0"> 52 | <Button 53 | color={ButtonColor.WhiteDanger} 54 | icon={ArrowLeftOnRectangleIcon} 55 | title={messages.signOut} 56 | onClick={() => setSignOutModalOpen(true)} 57 | /> 58 | </div> 59 | <Modal 60 | actions={ 61 | <> 62 | <Button color={ButtonColor.White} round onClick={() => setSignOutModalOpen(false)}> 63 | {messages.modals.signOut.actions.cancel} 64 | </Button> 65 | <Button.Link color={ButtonColor.Red} to$="signOut" round> 66 | {messages.modals.signOut.actions.confirm} 67 | </Button.Link> 68 | </> 69 | } 70 | open={signOutModalOpen} 71 | onClose={() => setSignOutModalOpen(false)} 72 | > 73 | <Modal.Aside> 74 | <IconCircle color={IconCircleColor.Red} icon={ArrowLeftOnRectangleIcon} /> 75 | </Modal.Aside> 76 | <Modal.Body> 77 | <Modal.Title>{messages.modals.signOut.title}</Modal.Title> 78 | <Modal.Description>{messages.modals.signOut.description}</Modal.Description> 79 | </Modal.Body> 80 | </Modal> 81 | </> 82 | ) : ( 83 | <> 84 | <Button.Link className="flex-1" color={ButtonColor.White} to$="signInShow"> 85 | {messages.signIn} 86 | </Button.Link> 87 | <Button.Link className="flex-1" color={ButtonColor.Emerald} to$="signUpShow"> 88 | {messages.signUp} 89 | </Button.Link> 90 | </> 91 | )} 92 | </div> 93 | ); 94 | } 95 | 96 | export default island("SidebarUserInfo", SidebarUserInfo); 97 | -------------------------------------------------------------------------------- /resources/entry.client.ts: -------------------------------------------------------------------------------- 1 | import { initClient } from "@microeinhundert/radonis"; 2 | 3 | initClient(); 4 | -------------------------------------------------------------------------------- /resources/hooks/useAuthenticatedUser.ts: -------------------------------------------------------------------------------- 1 | import { useGlobals } from "@microeinhundert/radonis"; 2 | 3 | export function useAuthenticatedUser() { 4 | const { authenticatedUser } = useGlobals(); 5 | 6 | return authenticatedUser; 7 | } 8 | -------------------------------------------------------------------------------- /resources/hooks/useCsrfToken.ts: -------------------------------------------------------------------------------- 1 | import { useGlobals } from "@microeinhundert/radonis"; 2 | 3 | export function useCsrfToken() { 4 | const { csrfToken } = useGlobals(); 5 | 6 | return csrfToken; 7 | } 8 | -------------------------------------------------------------------------------- /resources/hooks/useFormField.ts: -------------------------------------------------------------------------------- 1 | import { useFlashMessages } from "@microeinhundert/radonis"; 2 | import type { InputHTMLAttributes, TextareaHTMLAttributes } from "react"; 3 | import { useId, useState } from "react"; 4 | 5 | export function useFormField({ 6 | name, 7 | label, 8 | description, 9 | id, 10 | defaultValue, 11 | ...restProps 12 | }: (InputHTMLAttributes<HTMLInputElement> | TextareaHTMLAttributes<HTMLTextAreaElement>) & { 13 | name: string; 14 | label: string; 15 | description?: string; 16 | }) { 17 | const randomId = useId(); 18 | const { get$ } = useFlashMessages(); 19 | 20 | /** 21 | * Value and error states 22 | */ 23 | const [value, setValue] = useState(defaultValue ?? get$(name)); 24 | const [error, setError] = useState(get$(`errors.${name}`)); 25 | 26 | /** 27 | * Touched and dirty states 28 | */ 29 | const [touched, setTouched] = useState(false); 30 | const [dirty, setDirty] = useState(false); 31 | 32 | /** 33 | * IDs 34 | */ 35 | const inputId = id ?? randomId; 36 | const descriptionId = `description-${inputId}`; 37 | const errorId = `error-${inputId}`; 38 | 39 | const getInputProps = ( 40 | overrides?: InputHTMLAttributes<HTMLInputElement> | TextareaHTMLAttributes<HTMLTextAreaElement> 41 | ): Record<string, unknown> => ({ 42 | ...restProps, 43 | name, 44 | "id": inputId, 45 | "aria-invalid": error ? "true" : "false", 46 | "aria-describedby": error ? errorId : description ? descriptionId : undefined, 47 | ...overrides, 48 | "onChange": (event) => { 49 | (overrides ?? restProps)?.onChange?.(event); 50 | 51 | if (event.defaultPrevented) return; 52 | 53 | setDirty(true); 54 | setError(""); 55 | setValue(event.target.value); 56 | }, 57 | "onBlur": (event) => { 58 | (overrides ?? restProps)?.onBlur?.(event); 59 | 60 | if (event.defaultPrevented) return; 61 | 62 | setTouched(true); 63 | }, 64 | }); 65 | 66 | const getLabelProps = () => ({ 67 | htmlFor: inputId, 68 | }); 69 | 70 | const getDescriptionProps = () => ({ 71 | id: descriptionId, 72 | }); 73 | 74 | const getErrorProps = () => ({ 75 | id: errorId, 76 | }); 77 | 78 | return { 79 | name, 80 | label, 81 | description, 82 | // 83 | value, 84 | setValue, 85 | // 86 | error, 87 | setError, 88 | // 89 | touched, 90 | setTouched, 91 | // 92 | dirty, 93 | setDirty, 94 | // 95 | getInputProps, 96 | getLabelProps, 97 | getDescriptionProps, 98 | getErrorProps, 99 | }; 100 | } 101 | -------------------------------------------------------------------------------- /resources/hooks/useNavigation.ts: -------------------------------------------------------------------------------- 1 | import CogIcon from "@heroicons/react/24/outline/CogIcon"; 2 | import CubeIcon from "@heroicons/react/24/outline/CubeIcon"; 3 | import HomeIcon from "@heroicons/react/24/outline/HomeIcon"; 4 | 5 | import { useAuthenticatedUser } from "./useAuthenticatedUser"; 6 | import { useNavigationBuilder } from "./useNavigationBuilder"; 7 | 8 | export function useNavigation() { 9 | const user = useAuthenticatedUser(); 10 | const navigationBuilder = useNavigationBuilder(); 11 | 12 | return { 13 | primary: navigationBuilder.make([ 14 | { 15 | messageIdentifier$: "navigation.home", 16 | routeIdentifier$: "home", 17 | icon: HomeIcon, 18 | canAccess: () => !user, 19 | }, 20 | { 21 | messageIdentifier$: "navigation.dashboard", 22 | routeIdentifier$: "dashboard", 23 | icon: HomeIcon, 24 | canAccess: () => !!user, 25 | }, 26 | { 27 | messageIdentifier$: "navigation.gardens", 28 | routeIdentifier$: "gardens.index", 29 | icon: CubeIcon, 30 | canAccess: () => !!user, 31 | }, 32 | ]), 33 | secondary: navigationBuilder.make([ 34 | { 35 | messageIdentifier$: "navigation.settings", 36 | routeIdentifier$: "settings", 37 | icon: CogIcon, 38 | canAccess: () => !!user, 39 | }, 40 | ]), 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /resources/hooks/useNavigationBuilder.ts: -------------------------------------------------------------------------------- 1 | import { useI18n, useRoute, useUrlBuilder } from "@microeinhundert/radonis"; 2 | 3 | interface NavigationBuilderItem { 4 | messageIdentifier$: string; 5 | routeIdentifier$: string; 6 | icon?: IconComponent; 7 | canAccess?: () => boolean; 8 | } 9 | 10 | export interface NavigationItem { 11 | name: string; 12 | href: string; 13 | current: boolean; 14 | icon?: IconComponent; 15 | } 16 | 17 | export function useNavigationBuilder() { 18 | const { formatMessage$ } = useI18n(); 19 | const route = useRoute(); 20 | const urlBuilder = useUrlBuilder(); 21 | 22 | function make(items: NavigationBuilderItem[]) { 23 | return items 24 | .filter(({ canAccess }) => canAccess?.() ?? true) 25 | .map<NavigationItem>(({ messageIdentifier$, routeIdentifier$, icon }) => ({ 26 | name: formatMessage$(messageIdentifier$), 27 | href: urlBuilder.make$(routeIdentifier$), 28 | current: route.isCurrent$(routeIdentifier$), 29 | icon, 30 | })); 31 | } 32 | 33 | return { 34 | make, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /resources/lang/en/auth.json: -------------------------------------------------------------------------------- 1 | { 2 | "signIn": { 3 | "title": "Sign in to your account", 4 | "form": { 5 | "email": { 6 | "label": "Email" 7 | }, 8 | "password": { 9 | "label": "Password" 10 | }, 11 | "rememberMe": { 12 | "label": "Keep me signed in" 13 | }, 14 | "actions": { 15 | "submit": "Sign In" 16 | }, 17 | "signUpLinkLabel": "Not already signed up?" 18 | } 19 | }, 20 | "signUp": { 21 | "title": "Sign up for an account", 22 | "form": { 23 | "firstName": { 24 | "label": "First Name" 25 | }, 26 | "lastName": { 27 | "label": "Last Name" 28 | }, 29 | "email": { 30 | "label": "Email" 31 | }, 32 | "password": { 33 | "label": "Password" 34 | }, 35 | "passwordConfirmation": { 36 | "label": "Confirm Password" 37 | }, 38 | "actions": { 39 | "submit": "Sign Up" 40 | }, 41 | "signInLinkLabel": "Already signed up?" 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /resources/lang/en/dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Dashboard" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/lang/en/errors.json: -------------------------------------------------------------------------------- 1 | { 2 | "internalServerError": { 3 | "headline": "Internal Server Error", 4 | "text": "Something is not right. Please try again later. Cause: { message }" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /resources/lang/en/gardens.json: -------------------------------------------------------------------------------- 1 | { 2 | "form": { 3 | "name": { 4 | "label": "Name" 5 | }, 6 | "zip": { 7 | "label": "ZIP Code" 8 | }, 9 | "city": { 10 | "label": "City" 11 | }, 12 | "actions": { 13 | "create": "Create Garden", 14 | "update": "Update Garden" 15 | } 16 | }, 17 | "index": { 18 | "title": "Gardens", 19 | "actions": { 20 | "create": "Create Garden" 21 | } 22 | }, 23 | "create": { 24 | "title": "Create Garden" 25 | }, 26 | "show": { 27 | "title": "{ name }" 28 | }, 29 | "edit": { 30 | "title": "Edit \"{ name }\"" 31 | }, 32 | "list": { 33 | "actions": { 34 | "edit": "Edit Garden", 35 | "delete": "Delete Garden" 36 | }, 37 | "modals": { 38 | "delete": { 39 | "title": "Are you sure you want to delete the garden \"{ name }\"?", 40 | "description": "This action cannot be undone!", 41 | "actions": { 42 | "cancel": "Cancel", 43 | "confirm": "Delete Garden" 44 | } 45 | } 46 | }, 47 | "noData": { 48 | "headline": "No Gardens Added", 49 | "text": "Click the button in the upper right to add your first garden." 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /resources/lang/en/home.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Home" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/lang/en/navigation.json: -------------------------------------------------------------------------------- 1 | { 2 | "home": "Home", 3 | "dashboard": "Dashboard", 4 | "gardens": "Gardens", 5 | "settings": "Settings" 6 | } 7 | -------------------------------------------------------------------------------- /resources/lang/en/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": { 3 | "title": "Settings" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /resources/lang/en/shared.json: -------------------------------------------------------------------------------- 1 | { 2 | "input": { 3 | "showValue": "Show value", 4 | "hideValue": "Hide value" 5 | }, 6 | "search": { 7 | "label": "Search", 8 | "placeholder": "Search for something" 9 | }, 10 | "sidebar": { 11 | "signUp": "Sign Up", 12 | "signIn": "Sign In", 13 | "signOut": "Sign Out", 14 | "modals": { 15 | "signOut": { 16 | "title": "Are you sure you want to sign out?", 17 | "description": "You can of course sign in again at any time.", 18 | "actions": { 19 | "cancel": "Cancel", 20 | "confirm": "Sign Out" 21 | } 22 | } 23 | } 24 | }, 25 | "modal": { 26 | "close": "Close modal" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /resources/lang/en/validator.json: -------------------------------------------------------------------------------- 1 | { 2 | "invalidEmailOrPassword": "Either the email or the password you entered is incorrect.", 3 | "shared": { 4 | "required": "This field is required.", 5 | "unique": "The value of this field must be unique.", 6 | "minLength": "This field must have { minLength } items.", 7 | 8 | "email.email": "This field must contain a valid email.", 9 | "email.unique": "This email is already in use.", 10 | 11 | "password.minLength": "The password must contain at least { minLength } characters.", 12 | "passwordConfirmation.confirmed": "The passwords do not match." 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /resources/layouts/Auth.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import Logo from "../components/Logo"; 4 | 5 | interface AuthLayoutProps { 6 | children?: ReactNode; 7 | title: string; 8 | } 9 | 10 | function AuthLayout({ children, title }: AuthLayoutProps) { 11 | return ( 12 | <> 13 | <div className="flex min-h-screen flex-col justify-center bg-gray-100 py-12 sm:px-6 lg:px-8"> 14 | <div className="sm:mx-auto sm:w-full sm:max-w-md"> 15 | <Logo className="mx-auto h-12 w-auto" /> 16 | <h1 className="mt-6 text-center text-3xl font-extrabold text-gray-900">{title}</h1> 17 | </div> 18 | <div className="mt-14 sm:mx-auto sm:w-full sm:max-w-md"> 19 | <div className="border border-gray-200 bg-white py-8 px-4 sm:rounded-lg sm:px-10"> 20 | {children} 21 | </div> 22 | </div> 23 | </div> 24 | </> 25 | ); 26 | } 27 | 28 | export { AuthLayout }; 29 | -------------------------------------------------------------------------------- /resources/layouts/Base.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | 3 | import Search from "../components/Search.island"; 4 | import Sidebar from "../components/Sidebar"; 5 | 6 | interface BaseLayoutProps { 7 | children?: ReactNode; 8 | } 9 | 10 | function BaseLayout({ children }: BaseLayoutProps) { 11 | return ( 12 | <div className="min-h-screen bg-gray-100"> 13 | <div className="fixed inset-y-0 hidden w-64 lg:block"> 14 | <Sidebar /> 15 | </div> 16 | <div className="flex-1 lg:pl-64"> 17 | <div className="mx-auto max-w-6xl px-4 pb-12 sm:px-6 md:px-8"> 18 | <div className="mb-10 border-b border-gray-200 py-4"> 19 | <Search /> 20 | </div> 21 | <main>{children}</main> 22 | </div> 23 | </div> 24 | </div> 25 | ); 26 | } 27 | 28 | export { BaseLayout }; 29 | -------------------------------------------------------------------------------- /resources/utils/string.ts: -------------------------------------------------------------------------------- 1 | export function getFirstChar(string: string): string { 2 | return string.charAt(0); 3 | } 4 | 5 | export function capitalize(string: string): string { 6 | return getFirstChar(string).toUpperCase() + string.slice(1); 7 | } 8 | 9 | export function clsx(...classes: unknown[]): string { 10 | return classes.filter(Boolean).join(" "); 11 | } 12 | -------------------------------------------------------------------------------- /resources/views/Auth/SignIn.tsx: -------------------------------------------------------------------------------- 1 | import { HydrationRoot, useI18n } from "@microeinhundert/radonis"; 2 | import SignInForm from "Components/Auth/SignInForm.island"; 3 | import { AuthLayout } from "Layouts/Auth"; 4 | 5 | function SignIn() { 6 | const { formatMessage$ } = useI18n(); 7 | 8 | const messages = { 9 | title: formatMessage$("auth.signIn.title"), 10 | }; 11 | 12 | return ( 13 | <AuthLayout title={messages.title}> 14 | <HydrationRoot> 15 | <SignInForm /> 16 | </HydrationRoot> 17 | </AuthLayout> 18 | ); 19 | } 20 | 21 | export { SignIn }; 22 | -------------------------------------------------------------------------------- /resources/views/Auth/SignUp.tsx: -------------------------------------------------------------------------------- 1 | import { HydrationRoot, useI18n } from "@microeinhundert/radonis"; 2 | import SignUpForm from "Components/Auth/SignUpForm.island"; 3 | import { AuthLayout } from "Layouts/Auth"; 4 | 5 | function SignUp() { 6 | const { formatMessage$ } = useI18n(); 7 | 8 | const messages = { 9 | title: formatMessage$("auth.signUp.title"), 10 | }; 11 | 12 | return ( 13 | <AuthLayout title={messages.title}> 14 | <HydrationRoot> 15 | <SignUpForm /> 16 | </HydrationRoot> 17 | </AuthLayout> 18 | ); 19 | } 20 | 21 | export { SignUp }; 22 | -------------------------------------------------------------------------------- /resources/views/Auth/index.ts: -------------------------------------------------------------------------------- 1 | export { SignIn } from "./SignIn"; 2 | export { SignUp } from "./SignUp"; 3 | -------------------------------------------------------------------------------- /resources/views/Dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from "@microeinhundert/radonis"; 2 | import Header from "Components/Header.island"; 3 | import { BaseLayout } from "Layouts/Base"; 4 | 5 | function Index() { 6 | const { formatMessage$ } = useI18n(); 7 | 8 | const messages = { 9 | title: formatMessage$("dashboard.index.title"), 10 | }; 11 | 12 | return ( 13 | <BaseLayout> 14 | <Header title={messages.title} /> 15 | <p>This is an empty view.</p> 16 | </BaseLayout> 17 | ); 18 | } 19 | 20 | export { Index }; 21 | -------------------------------------------------------------------------------- /resources/views/Errors/InternalServerError.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from "@microeinhundert/radonis"; 2 | import Fallback from "Components/Fallback"; 3 | import ErrorIllustration from "Components/Illustrations/ErrorIllustration"; 4 | 5 | interface InternalServerErrorProps { 6 | error: unknown; 7 | } 8 | 9 | function InternalServerError({ error }: InternalServerErrorProps) { 10 | const { formatMessage$ } = useI18n(); 11 | 12 | const messages = { 13 | headline: formatMessage$("errors.internalServerError.headline"), 14 | text: formatMessage$("errors.internalServerError.text", { 15 | message: error instanceof Error ? error.message : "-", 16 | }), 17 | }; 18 | 19 | return ( 20 | <div className="h-screen"> 21 | <Fallback {...messages} icon={ErrorIllustration} /> 22 | </div> 23 | ); 24 | } 25 | 26 | export { InternalServerError }; 27 | -------------------------------------------------------------------------------- /resources/views/Gardens/Create.tsx: -------------------------------------------------------------------------------- 1 | import { HydrationRoot, useI18n } from "@microeinhundert/radonis"; 2 | import GardenForm from "Components/Gardens/GardenForm.island"; 3 | import Header from "Components/Header.island"; 4 | import { BaseLayout } from "Layouts/Base"; 5 | 6 | function Create() { 7 | const { formatMessage$ } = useI18n(); 8 | 9 | const messages = { 10 | title: formatMessage$("gardens.create.title"), 11 | }; 12 | 13 | return ( 14 | <BaseLayout> 15 | <Header title={messages.title} /> 16 | <HydrationRoot> 17 | <GardenForm /> 18 | </HydrationRoot> 19 | </BaseLayout> 20 | ); 21 | } 22 | 23 | export { Create }; 24 | -------------------------------------------------------------------------------- /resources/views/Gardens/Edit.tsx: -------------------------------------------------------------------------------- 1 | import { HydrationRoot, useI18n } from "@microeinhundert/radonis"; 2 | import type Garden from "App/Models/Garden"; 3 | import GardenForm from "Components/Gardens/GardenForm.island"; 4 | import Header from "Components/Header.island"; 5 | import { BaseLayout } from "Layouts/Base"; 6 | 7 | interface EditProps { 8 | garden: Garden; 9 | } 10 | 11 | function Edit({ garden }: EditProps) { 12 | const { formatMessage$ } = useI18n(); 13 | 14 | const messages = { 15 | title: formatMessage$("gardens.edit.title", { name: garden.name }), 16 | }; 17 | 18 | return ( 19 | <BaseLayout> 20 | <Header title={messages.title} /> 21 | <HydrationRoot> 22 | <GardenForm garden={garden} /> 23 | </HydrationRoot> 24 | </BaseLayout> 25 | ); 26 | } 27 | 28 | export { Edit }; 29 | -------------------------------------------------------------------------------- /resources/views/Gardens/Show.tsx: -------------------------------------------------------------------------------- 1 | import { BaseLayout } from "Layouts/Base"; 2 | 3 | function Show() { 4 | return <BaseLayout>Show</BaseLayout>; 5 | } 6 | 7 | export { Show }; 8 | -------------------------------------------------------------------------------- /resources/views/Gardens/index.tsx: -------------------------------------------------------------------------------- 1 | import PlusIcon from "@heroicons/react/24/solid/PlusIcon"; 2 | import { HydrationRoot, useI18n } from "@microeinhundert/radonis"; 3 | import type Garden from "App/Models/Garden"; 4 | import Button, { ButtonColor } from "Components/Button.island"; 5 | import GardensList from "Components/Gardens/GardensList.island"; 6 | import Header from "Components/Header.island"; 7 | import { BaseLayout } from "Layouts/Base"; 8 | 9 | interface IndexProps { 10 | gardens: Garden[]; 11 | } 12 | 13 | function Index({ gardens }: IndexProps) { 14 | const { formatMessage$ } = useI18n(); 15 | 16 | const messages = { 17 | title: formatMessage$("gardens.index.title"), 18 | actions: { 19 | create: formatMessage$("gardens.index.actions.create"), 20 | }, 21 | }; 22 | 23 | return ( 24 | <BaseLayout> 25 | <Header 26 | actions={ 27 | <> 28 | <Button.Link color={ButtonColor.Emerald} icon={PlusIcon} to$="gardens.create"> 29 | {messages.actions.create} 30 | </Button.Link> 31 | </> 32 | } 33 | title={messages.title} 34 | /> 35 | <HydrationRoot> 36 | <GardensList gardens={gardens} /> 37 | </HydrationRoot> 38 | </BaseLayout> 39 | ); 40 | } 41 | 42 | export { Index }; 43 | export { Create } from "./Create"; 44 | export { Edit } from "./Edit"; 45 | export { Show } from "./Show"; 46 | -------------------------------------------------------------------------------- /resources/views/Home/index.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from "@microeinhundert/radonis"; 2 | import Header from "Components/Header.island"; 3 | import { BaseLayout } from "Layouts/Base"; 4 | 5 | function Index() { 6 | const { formatMessage$ } = useI18n(); 7 | 8 | const messages = { 9 | title: formatMessage$("home.index.title"), 10 | }; 11 | 12 | return ( 13 | <BaseLayout> 14 | <Header title={messages.title} /> 15 | <p>This is an empty view.</p> 16 | </BaseLayout> 17 | ); 18 | } 19 | 20 | export { Index }; 21 | -------------------------------------------------------------------------------- /resources/views/Settings/index.tsx: -------------------------------------------------------------------------------- 1 | import { useI18n } from "@microeinhundert/radonis"; 2 | import Header from "Components/Header.island"; 3 | import { BaseLayout } from "Layouts/Base"; 4 | 5 | function Index() { 6 | const { formatMessage$ } = useI18n(); 7 | 8 | const messages = { 9 | title: formatMessage$("settings.index.title"), 10 | }; 11 | 12 | return ( 13 | <BaseLayout> 14 | <Header title={messages.title} /> 15 | <p>This is an empty view.</p> 16 | </BaseLayout> 17 | ); 18 | } 19 | 20 | export { Index }; 21 | -------------------------------------------------------------------------------- /server.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | AdonisJs Server 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file is meant to bootstrap the AdonisJs application 7 | | and start the HTTP server to accept incoming connections. You must avoid 8 | | making this file dirty and instead make use of `lifecycle hooks` provided 9 | | by AdonisJs service providers for custom code. 10 | | 11 | */ 12 | 13 | import "reflect-metadata"; 14 | 15 | import { Ignitor } from "@adonisjs/core/build/standalone"; 16 | import sourceMapSupport from "source-map-support"; 17 | 18 | sourceMapSupport.install({ handleUncaughtExceptions: false }); 19 | 20 | new Ignitor(__dirname).httpServer().start(); 21 | -------------------------------------------------------------------------------- /start/bouncer.ts: -------------------------------------------------------------------------------- 1 | import Bouncer from "@ioc:Adonis/Addons/Bouncer"; 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Bouncer Actions 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Actions allows you to separate your application business logic from the 9 | | authorization logic. Feel free to make use of policies when you find 10 | | yourself creating too many actions 11 | | 12 | | You can define an action using the `.define` method on the Bouncer object 13 | | as shown in the following example 14 | | 15 | | ``` 16 | | Bouncer.define('deletePost', (user: User, post: Post) => { 17 | | return post.user_id === user.id 18 | | }) 19 | | ``` 20 | | 21 | */ 22 | export const { actions } = Bouncer; 23 | 24 | /* 25 | |-------------------------------------------------------------------------- 26 | | Bouncer Policies 27 | |-------------------------------------------------------------------------- 28 | | 29 | | Policies are self contained actions for a given resource. For example: You 30 | | can create a policy for a "User" resource, one policy for a "Post" resource 31 | | and so on. 32 | | 33 | | The "registerPolicies" accepts a unique policy name and a function to lazy 34 | | import the policy 35 | | 36 | | ``` 37 | | Bouncer.registerPolicies({ 38 | | UserPolicy: () => import('App/Policies/UserPolicy'), 39 | | PostPolicy: () => import('App/Policies/PostPolicy') 40 | | }) 41 | | ``` 42 | | 43 | */ 44 | export const { policies } = Bouncer.registerPolicies({ 45 | GardenPolicy: () => import("App/Policies/GardenPolicy"), 46 | }); 47 | -------------------------------------------------------------------------------- /start/kernel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Application middleware 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is used to define middleware for HTTP requests. You can register 7 | | middleware as a `closure` or an IoC container binding. The bindings are 8 | | preferred, since they keep this file clean. 9 | | 10 | */ 11 | import Server from "@ioc:Adonis/Core/Server"; 12 | 13 | /* 14 | |-------------------------------------------------------------------------- 15 | | Global middleware 16 | |-------------------------------------------------------------------------- 17 | | 18 | | An array of global middleware, that will be executed in the order they 19 | | are defined for every HTTP requests. 20 | | 21 | */ 22 | Server.middleware.register([ 23 | () => import("@ioc:Adonis/Addons/Shield"), 24 | () => import("@ioc:Adonis/Core/BodyParser"), 25 | () => import("App/Middleware/ErrorPages"), 26 | () => import("App/Middleware/Csrf"), 27 | () => import("App/Middleware/DetectUserLocale"), 28 | () => import("App/Middleware/SilentAuth"), 29 | ]); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Named middleware 34 | |-------------------------------------------------------------------------- 35 | | 36 | | Named middleware are defined as key-value pair. The value is the namespace 37 | | or middleware function and key is the alias. Later you can use these 38 | | alias on individual routes. For example: 39 | | 40 | | { auth: () => import('App/Middleware/Auth') } 41 | | 42 | | and then use it as follows 43 | | 44 | | Route.get('dashboard', 'UserController.dashboard').middleware('auth') 45 | | 46 | */ 47 | Server.middleware.registerNamed({ auth: () => import("App/Middleware/Auth") }); 48 | -------------------------------------------------------------------------------- /start/routes.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Routes 4 | |-------------------------------------------------------------------------- 5 | | 6 | | This file is dedicated for defining HTTP routes. A single file is enough 7 | | for majority of projects, however you can define routes in different 8 | | files and just make sure to import them inside this file. For example 9 | | 10 | | Define routes in following two files 11 | | ├── start/routes/cart.ts 12 | | ├── start/routes/customer.ts 13 | | 14 | | and then import them inside `start/routes.ts` as follows 15 | | 16 | | import './routes/cart' 17 | | import './routes/customer' 18 | | 19 | */ 20 | 21 | import Route from "@ioc:Adonis/Core/Route"; 22 | 23 | Route.get("/", "HomeController.index").as("home"); 24 | 25 | Route.group(() => { 26 | Route.get("/dashboard", "DashboardController.index").as("dashboard"); 27 | Route.get("/settings", "SettingsController.index").as("settings"); 28 | Route.resource("gardens", "GardensController").as("gardens"); 29 | }).middleware("auth"); 30 | 31 | Route.get("/signUp", "AuthController.signUpShow").as("signUpShow"); 32 | Route.post("/signUp", "AuthController.signUp").as("signUp"); 33 | 34 | Route.get("/signIn", "AuthController.signInShow").as("signInShow"); 35 | Route.post("/signIn", "AuthController.signIn").as("signIn"); 36 | 37 | Route.get("/signOut", "AuthController.signOut").as("signOut"); 38 | -------------------------------------------------------------------------------- /test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | |-------------------------------------------------------------------------- 3 | | Tests 4 | |-------------------------------------------------------------------------- 5 | | 6 | | The contents in this file boots the AdonisJS application and configures 7 | | the Japa tests runner. 8 | | 9 | | For the most part you will never edit this file. The configuration 10 | | for the tests can be controlled via ".adonisrc.json" and 11 | | "tests/bootstrap.ts" files. 12 | | 13 | */ 14 | 15 | process.env.NODE_ENV = "test"; 16 | 17 | import "reflect-metadata"; 18 | 19 | import { Ignitor } from "@adonisjs/core/build/standalone"; 20 | import type { RunnerHooksHandler } from "@japa/runner"; 21 | import { configure, processCliArgs, run } from "@japa/runner"; 22 | import sourceMapSupport from "source-map-support"; 23 | 24 | sourceMapSupport.install({ handleUncaughtExceptions: false }); 25 | 26 | const kernel = new Ignitor(__dirname).kernel("test"); 27 | 28 | kernel 29 | .boot() 30 | .then(() => import("./tests/bootstrap")) 31 | .then(({ runnerHooks, ...config }) => { 32 | const app: RunnerHooksHandler[] = [() => kernel.start()]; 33 | 34 | configure({ 35 | ...kernel.application.rcFile.tests, 36 | ...processCliArgs(process.argv.slice(2)), 37 | ...config, 38 | ...{ 39 | importer: (filePath) => import(filePath), 40 | setup: app.concat(runnerHooks.setup), 41 | teardown: runnerHooks.teardown, 42 | }, 43 | cwd: kernel.application.appRoot, 44 | }); 45 | 46 | run(); 47 | }); 48 | -------------------------------------------------------------------------------- /tests/bootstrap.ts: -------------------------------------------------------------------------------- 1 | import TestUtils from "@ioc:Adonis/Core/TestUtils"; 2 | import { apiClient, assert, runFailedTests, specReporter } from "@japa/preset-adonis"; 3 | import type { Config } from "@japa/runner"; 4 | 5 | /* 6 | |-------------------------------------------------------------------------- 7 | | Japa Plugins 8 | |-------------------------------------------------------------------------- 9 | | 10 | | Japa plugins allows you to add additional features to Japa. By default 11 | | we register the assertion plugin. 12 | | 13 | | Feel free to remove existing plugins or add more. 14 | | 15 | */ 16 | export const plugins: Config["plugins"] = [assert(), runFailedTests(), apiClient()]; 17 | 18 | /* 19 | |-------------------------------------------------------------------------- 20 | | Japa Reporters 21 | |-------------------------------------------------------------------------- 22 | | 23 | | Japa reporters displays/saves the progress of tests as they are executed. 24 | | By default, we register the spec reporter to show a detailed report 25 | | of tests on the terminal. 26 | | 27 | */ 28 | export const reporters: Config["reporters"] = [specReporter()]; 29 | 30 | /* 31 | |-------------------------------------------------------------------------- 32 | | Runner hooks 33 | |-------------------------------------------------------------------------- 34 | | 35 | | Runner hooks are executed after booting the AdonisJS app and 36 | | before the test files are imported. 37 | | 38 | | You can perform actions like starting the HTTP server or running migrations 39 | | within the runner hooks 40 | | 41 | */ 42 | export const runnerHooks: Required<Pick<Config, "setup" | "teardown">> = { 43 | setup: [() => TestUtils.ace().loadCommands()], 44 | teardown: [], 45 | }; 46 | 47 | /* 48 | |-------------------------------------------------------------------------- 49 | | Configure individual suites 50 | |-------------------------------------------------------------------------- 51 | | 52 | | The configureSuite method gets called for every test suite registered 53 | | within ".adonisrc.json" file. 54 | | 55 | | You can use this method to configure suites. For example: Only start 56 | | the HTTP server when it is a functional suite. 57 | */ 58 | export const configureSuite: Config["configureSuite"] = (suite) => { 59 | if (suite.name === "functional") { 60 | suite.setup(() => TestUtils.httpServer().start()); 61 | } 62 | }; 63 | -------------------------------------------------------------------------------- /tests/functional/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/microeinhundert/radonis-example-application/d62099061b8cd71541608fb8fb03fe9aeb172894/tests/functional/.gitkeep -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./node_modules/adonis-preset-ts/tsconfig", 3 | "include": ["**/*"], 4 | "exclude": ["node_modules", "build"], 5 | "compilerOptions": { 6 | "outDir": "build", 7 | "rootDir": "./", 8 | "sourceMap": true, 9 | "jsx": "react-jsx", 10 | "lib": ["dom", "dom.iterable", "esnext"], 11 | "paths": { 12 | "App/*": ["./app/*"], 13 | "Config/*": ["./config/*"], 14 | "Contracts/*": ["./contracts/*"], 15 | "Database/*": ["./database/*"], 16 | "Views/*": ["./resources/views/*"], 17 | "Components/*": ["./resources/components/*"], 18 | "Layouts/*": ["./resources/layouts/*"], 19 | "resources/*": ["./resources/*"] 20 | }, 21 | "types": [ 22 | "@adonisjs/core", 23 | "@adonisjs/repl", 24 | "@adonisjs/session", 25 | "@adonisjs/shield", 26 | "@adonisjs/lucid", 27 | "@adonisjs/auth", 28 | "@adonisjs/i18n", 29 | "@adonisjs/redis", 30 | "@adonisjs/mail", 31 | "@adonisjs/drive-s3", 32 | "@adonisjs/bouncer", 33 | "@japa/preset-adonis/build/adonis-typings", 34 | "@microeinhundert/radonis-server" 35 | ] 36 | } 37 | } 38 | --------------------------------------------------------------------------------