diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml index a468d02..53f0d07 100644 --- a/.github/workflows/website.yml +++ b/.github/workflows/website.yml @@ -14,7 +14,7 @@ jobs: lint: name: Linting runs-on: ubuntu-latest - + defaults: run: working-directory: website @@ -46,7 +46,7 @@ jobs: typecheck: name: Typechecking runs-on: ubuntu-latest - + defaults: run: working-directory: website diff --git a/website/.env.example b/website/.env.example index 51e84b4..c0d59ec 100644 --- a/website/.env.example +++ b/website/.env.example @@ -6,7 +6,7 @@ APP_KEY= NODE_ENV=development SESSION_DRIVER=cookie -FROM_EMAIL= +FROM_EMAIL=noreply@eneiconf.pt SMTP_HOST=localhost SMTP_PORT=1025 diff --git a/website/.gitignore b/website/.gitignore index 4e9b445..1c92932 100644 --- a/website/.gitignore +++ b/website/.gitignore @@ -2,6 +2,7 @@ node_modules build tmp +.adonisjs # Secrets .env diff --git a/website/README.md b/website/README.md index bc1baf3..f004ee4 100644 --- a/website/README.md +++ b/website/README.md @@ -30,24 +30,16 @@ To get started with the website, follow these steps: 3. Install the dependencies: ```bash - pnpm install + pnpm install --frozen-lockfile ``` -4. Copy the `.env.example` file to a new file called `.env` and update the values as needed. - -5. Create an app key to sign the session cookies: - - ```bash - node ace generate:key - ``` - -6. Run the database migrations to create the database: +4. Run the database migrations to create the database: ```bash node ace migration:run ``` -7. Start the development server: +5. Start the development server: ```bash pnpm run dev @@ -55,3 +47,18 @@ To get started with the website, follow these steps: This will start the development server on `http://localhost:3333`. The development server has hot-reloading enabled, so you can make changes to the code and see them reflected in the browser immediately. + +## Defining Routes with Tuyau + +To use Tuyau in your routes, you need to update the API file each time you do certain actions, mainly: + +1. Adding a new route/controller to your project +2. Adding a `request.validateUsing` call in your controller method + +This is done with the command: + +```bash +node ace tuyau:generate +``` + +More info here: [Tuyau - Installation](https://adonisjs.com/blog/introducing-tuyau#installation) diff --git a/website/adonisrc.ts b/website/adonisrc.ts index 3fb896b..8a1efd4 100644 --- a/website/adonisrc.ts +++ b/website/adonisrc.ts @@ -14,6 +14,7 @@ export default defineConfig({ () => import('@adonisjs/core/commands'), () => import('@adonisjs/lucid/commands'), () => import('@adonisjs/mail/commands'), + () => import('@tuyau/core/commands'), ], /* @@ -43,6 +44,7 @@ export default defineConfig({ () => import('@adonisjs/auth/auth_provider'), () => import('@adonisjs/inertia/inertia_provider'), () => import('@adonisjs/mail/mail_provider'), + () => import('@tuyau/core/tuyau_provider'), ], /* diff --git a/website/app/controllers/tickets_controller.ts b/website/app/controllers/tickets_controller.ts new file mode 100644 index 0000000..1a3a5be --- /dev/null +++ b/website/app/controllers/tickets_controller.ts @@ -0,0 +1,10 @@ +import Ticket from '#models/ticket' +import type { HttpContext } from '@adonisjs/core/http' + +export default class TicketsController { + async index({ inertia }: HttpContext) { + const ticketTypes = await Ticket.all() + + return inertia.render('tickets', { ticketTypes }) + } +} diff --git a/website/app/models/ticket.ts b/website/app/models/ticket.ts new file mode 100644 index 0000000..049af34 --- /dev/null +++ b/website/app/models/ticket.ts @@ -0,0 +1,25 @@ +import { DateTime } from 'luxon' +import { BaseModel, column } from '@adonisjs/lucid/orm' + +export default class Ticket extends BaseModel { + @column({ isPrimary: true }) + declare id: number + + @column() + declare name: string | null + + @column() + declare description: string + + @column() + declare price: number + + @column() + declare stock: number + + @column.dateTime({ autoCreate: true }) + declare createdAt: DateTime + + @column.dateTime({ autoCreate: true, autoUpdate: true }) + declare updatedAt: DateTime | null +} diff --git a/website/config/tuyau.ts b/website/config/tuyau.ts new file mode 100644 index 0000000..dd04438 --- /dev/null +++ b/website/config/tuyau.ts @@ -0,0 +1,17 @@ +import { defineConfig } from '@tuyau/core' + +const tuyauConfig = defineConfig({ + codegen: { + /** + * Filters the definitions and named routes to be generated + */ + // definitions: { + // only: [], + // } + // routes: { + // only: [], + // } + }, +}) + +export default tuyauConfig diff --git a/website/database/migrations/1734798835308_create_tickets_table.ts b/website/database/migrations/1734798835308_create_tickets_table.ts new file mode 100644 index 0000000..f6ddda6 --- /dev/null +++ b/website/database/migrations/1734798835308_create_tickets_table.ts @@ -0,0 +1,21 @@ +import { BaseSchema } from '@adonisjs/lucid/schema' + +export default class extends BaseSchema { + protected tableName = 'tickets' + + async up() { + this.schema.createTable(this.tableName, (table) => { + table.increments('id') + table.string('name').nullable() + table.text('description') + table.float('price').notNullable() + table.integer('stock').notNullable() + table.timestamp('created_at') + table.timestamp('updated_at') + }) + } + + async down() { + this.schema.dropTable(this.tableName) + } +} diff --git a/website/database/seeders/ticket_seeder.ts b/website/database/seeders/ticket_seeder.ts new file mode 100644 index 0000000..6e8e514 --- /dev/null +++ b/website/database/seeders/ticket_seeder.ts @@ -0,0 +1,23 @@ +import { BaseSeeder } from '@adonisjs/lucid/seeders' +import Ticket from '#models/ticket' + +export default class extends BaseSeeder { + async run() { + await Ticket.createMany([ + { + name: 'Bilhete - Com Alojamento', + description: + 'Inclui:\n• Pequenos-almoços, almoços e jantares durante o período do evento\n• Acesso a coffee breaks e sessão de cocktails\n• Acesso a workshops, palestras e outros\n• Acesso a festas noturnas e outras atividades recreativas (exceto Rally Tascas) \n• Alojamento em Pavilhão', + price: 35, + stock: 150, + }, + { + name: 'Bilhete - Sem Alojamento', + description: + 'Inclui:\n• Pequenos-almoços, almoços e jantares durante o período do evento\n• Acesso a coffee breaks e sessão de cocktails\n• Acesso a workshops, palestras e outros\n• Acesso a festas noturnas e outras atividades recreativas (exceto Rally Tascas)', + price: 30, + stock: 50, + }, + ]) + } +} diff --git a/website/inertia/app/app.tsx b/website/inertia/app/app.tsx index 83ae4e7..819dd7d 100644 --- a/website/inertia/app/app.tsx +++ b/website/inertia/app/app.tsx @@ -3,9 +3,11 @@ import '../css/app.css' -import { hydrateRoot } from 'react-dom/client' -import { createInertiaApp } from '@inertiajs/react' import { resolvePageComponent } from '@adonisjs/inertia/helpers' +import { createInertiaApp } from '@inertiajs/react' +import { TuyauProvider } from '@tuyau/inertia/react' +import { hydrateRoot } from 'react-dom/client' +import { tuyau } from './tuyau' const appName = import.meta.env.VITE_APP_NAME || 'ENEI' @@ -19,6 +21,13 @@ createInertiaApp({ }, setup({ el, App, props }) { - hydrateRoot(el, ) + hydrateRoot( + el, + <> + + + + + ) }, }) diff --git a/website/inertia/app/ssr.tsx b/website/inertia/app/ssr.tsx index ca87be8..cf5098f 100644 --- a/website/inertia/app/ssr.tsx +++ b/website/inertia/app/ssr.tsx @@ -1,5 +1,7 @@ import ReactDOMServer from 'react-dom/server' import { createInertiaApp } from '@inertiajs/react' +import { TuyauProvider } from '@tuyau/inertia/react' +import { tuyau } from './tuyau' export default function render(page: any) { return createInertiaApp({ @@ -9,6 +11,12 @@ export default function render(page: any) { const pages = import.meta.glob('../pages/**/*.tsx', { eager: true }) return pages[`../pages/${name}.tsx`] }, - setup: ({ App, props }) => , + setup: ({ App, props }) => ( + <> + + + + + ), }) } diff --git a/website/inertia/app/tuyau.ts b/website/inertia/app/tuyau.ts new file mode 100644 index 0000000..a276e92 --- /dev/null +++ b/website/inertia/app/tuyau.ts @@ -0,0 +1,7 @@ +import { createTuyau } from '@tuyau/client' +import { api } from '#.adonisjs/api' + +export const tuyau = createTuyau({ + api, + baseUrl: import.meta.env.BASE_URL || 'http://localhost:3333', +}) diff --git a/website/inertia/pages/tickets.tsx b/website/inertia/pages/tickets.tsx new file mode 100644 index 0000000..0972394 --- /dev/null +++ b/website/inertia/pages/tickets.tsx @@ -0,0 +1,38 @@ +'use client' + +import { InferPageProps } from '@adonisjs/inertia/types' +import { Link } from '@tuyau/inertia/react' +import { Card, CardDescription, CardHeader, CardTitle } from '~/components/ui/card' +import TicketsController from '#controllers/tickets_controller' + +export default function SelectTicketsPage(props: InferPageProps) { + const imageSrc = `favicon.svg` + + return ( +
+

Seleciona o teu bilhete

+

+ Seleciona o teu bilhete e clica em comprar para continuar. +

+ +
+ {props.ticketTypes.map((ticket) => ( + + +
+ + {ticket.name} + + {ticket.description} + +

{ticket.price}€

+
+ {ticket.name +
+
+ + ))} +
+
+ ) +} diff --git a/website/package.json b/website/package.json index bf960d6..3c34ff7 100644 --- a/website/package.json +++ b/website/package.json @@ -13,9 +13,10 @@ "lint": "eslint .", "format": "prettier --write .", "typecheck": "tsc --noEmit", - "prepare": "cd .. && husky website/.husky" + "prepare": "sh -c './scripts/prepare.sh'" }, "imports": { + "#.adonisjs/*": "./.adonisjs/*.ts", "#controllers/*": "./app/controllers/*.js", "#exceptions/*": "./app/exceptions/*.js", "#models/*": "./app/models/*.js", @@ -103,6 +104,9 @@ "@radix-ui/react-toggle": "^1.1.1", "@radix-ui/react-toggle-group": "^1.1.1", "@radix-ui/react-tooltip": "^1.1.5", + "@tuyau/client": "^0.2.2", + "@tuyau/core": "^0.2.3", + "@tuyau/inertia": "^0.0.8", "@vinejs/vine": "^3.0.0", "better-sqlite3": "^11.7.0", "class-variance-authority": "^0.7.1", diff --git a/website/pnpm-lock.yaml b/website/pnpm-lock.yaml index 832c4e2..5b4ab3e 100644 --- a/website/pnpm-lock.yaml +++ b/website/pnpm-lock.yaml @@ -134,6 +134,15 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.1.5 version: 1.1.5(@types/react-dom@19.0.2(@types/react@19.0.1))(@types/react@19.0.1)(react-dom@19.0.0(react@19.0.0))(react@19.0.0) + '@tuyau/client': + specifier: ^0.2.2 + version: 0.2.2 + '@tuyau/core': + specifier: ^0.2.3 + version: 0.2.3(@adonisjs/core@6.17.0(@adonisjs/assembler@7.8.2(typescript@5.7.2))(@vinejs/vine@3.0.0)(edge.js@6.2.0)) + '@tuyau/inertia': + specifier: ^0.0.8 + version: 0.0.8(@inertiajs/react@2.0.0(react@19.0.0))(@tuyau/client@0.2.2)(react@19.0.0) '@vinejs/vine': specifier: ^3.0.0 version: 3.0.0 @@ -2256,6 +2265,33 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@tuyau/client@0.2.2': + resolution: {integrity: sha512-R2QS6N3Mp5s8672mdsYt7k9nIFnG5CwIk0XpSNJXpgbvQmU71cUI4ngoZUyOYV2USOo69SSYw9OS4k7y3CCzKg==} + + '@tuyau/core@0.2.3': + resolution: {integrity: sha512-iGqDpoB0ztqFzwIDQYiLp1V8J3AspgrGSTk/F3gB2agYIjd40UdZ1bkrArTpKpNQ1afdSxXVvL6LLqKhyCz/Pg==} + engines: {node: '>=20.6.0'} + peerDependencies: + '@adonisjs/core': ^6.2.0 + + '@tuyau/inertia@0.0.8': + resolution: {integrity: sha512-KAGx8s7CjruYkU1tJIS0Iq52IyM9zt/H/CweuLEN7Xtihs2xs47yy70+61NcHcuYJlCAGZzWVSL7FC2iK0HZVA==} + peerDependencies: + '@inertiajs/react': ^1.0.0 || ^2.0.0 + '@inertiajs/vue3': ^1.0.0 || ^2.0.0 + '@tuyau/client': 0.2.2 + react: ^18.0.0 || ^19.0.0 + vue: ^3.0.0 + peerDependenciesMeta: + '@inertiajs/react': + optional: true + '@inertiajs/vue3': + optional: true + react: + optional: true + vue: + optional: true + '@tuyau/utils@0.0.6': resolution: {integrity: sha512-X6teHJyGGyjkSHRvBf9tX13K54O6yxaDcNI/NbgXYb/yBdm4Esr4yRBYjOKEvaQYkAChQh/CXyEeGYNPdj+2Zg==} @@ -4237,6 +4273,10 @@ packages: tedious: optional: true + ky@1.7.4: + resolution: {integrity: sha512-zYEr/gh7uLW2l4su11bmQ2M9xLgQLjyvx58UyNM/6nuqyWFHPX5ktMjvpev3F8QWdjSsHUpnWew4PBCswBNuMQ==} + engines: {node: '>=18'} + lazy@1.0.11: resolution: {integrity: sha512-Y+CjUfLmIpoUCCRl0ub4smrYtGGr5AOa2AKOaWelGHOGz33X/Y/KizefGqbkwfz44+cnq/+9habclf8vOmu2LA==} engines: {node: '>=0.2.0'} @@ -4579,6 +4619,9 @@ packages: resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} engines: {node: '>= 0.4'} + object-to-formdata@4.5.1: + resolution: {integrity: sha512-QiM9D0NiU5jV6J6tjE1g7b4Z2tcUnKs1OPUi4iMb2zH+7jwlcUrASghgkFk9GtzqNNq8rTQJtT8AzjBAvLoNMw==} + object.assign@4.1.7: resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} engines: {node: '>= 0.4'} @@ -5526,8 +5569,8 @@ packages: resolution: {integrity: sha512-vrozgXDQwYO72vHjUb/HnFbQx1exDjoKzqx23aXEg2a9VIg2TSFZ8FmeZpTjUCFMYw7mpX4BE2SFu8wI7asYsw==} engines: {node: ^14.18.0 || >=16.0.0} - systeminformation@5.23.17: - resolution: {integrity: sha512-jjcPB23YcoNN43Q+A0rPFsGMMSAW/Zeok8rMGko1DvO8b1SO7zizgAEVQ4lyJdlAuc6vTmqnkyMCDD3N9Uwksw==} + systeminformation@5.23.23: + resolution: {integrity: sha512-QhEWrMFZnzWjFZ7J65gikIXTrB8U6b7VTQ8pLaF/GUgJaJoUoSuucqalIVj91D/grhRUtXplL6qYwTn1A4FfhQ==} engines: {node: '>=8.0.0'} os: [darwin, linux, win32, freebsd, openbsd, netbsd, sunos, android] hasBin: true @@ -8453,6 +8496,26 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@tuyau/client@0.2.2': + dependencies: + '@poppinss/matchit': 3.1.2 + '@tuyau/utils': 0.0.6 + ky: 1.7.4 + object-to-formdata: 4.5.1 + + '@tuyau/core@0.2.3(@adonisjs/core@6.17.0(@adonisjs/assembler@7.8.2(typescript@5.7.2))(@vinejs/vine@3.0.0)(edge.js@6.2.0))': + dependencies: + '@adonisjs/core': 6.17.0(@adonisjs/assembler@7.8.2(typescript@5.7.2))(@vinejs/vine@3.0.0)(edge.js@6.2.0) + '@tuyau/utils': 0.0.6 + ts-morph: 23.0.0 + + '@tuyau/inertia@0.0.8(@inertiajs/react@2.0.0(react@19.0.0))(@tuyau/client@0.2.2)(react@19.0.0)': + dependencies: + '@tuyau/client': 0.2.2 + optionalDependencies: + '@inertiajs/react': 2.0.0(react@19.0.0) + react: 19.0.0 + '@tuyau/utils@0.0.6': {} '@types/babel__core@7.20.5': @@ -10446,6 +10509,8 @@ snapshots: transitivePeerDependencies: - supports-color + ky@1.7.4: {} + lazy@1.0.11: {} levn@0.4.1: @@ -10732,6 +10797,8 @@ snapshots: object-keys@1.1.1: {} + object-to-formdata@4.5.1: {} + object.assign@4.1.7: dependencies: call-bind: 1.0.8 @@ -11004,7 +11071,7 @@ snapshots: async: 3.2.6 debug: 4.4.0 pidusage: 2.0.21 - systeminformation: 5.23.17 + systeminformation: 5.23.23 tx2: 1.0.5 transitivePeerDependencies: - supports-color @@ -11849,7 +11916,7 @@ snapshots: '@pkgr/core': 0.1.1 tslib: 2.8.1 - systeminformation@5.23.17: + systeminformation@5.23.23: optional: true tailwind-merge@2.5.5: {} diff --git a/website/scripts/prepare.sh b/website/scripts/prepare.sh new file mode 100755 index 0000000..1d5b65e --- /dev/null +++ b/website/scripts/prepare.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -e + +# Change current working directory to the root of the repository + +cd "$(dirname "$0")" +cd ../.. + +# Install Husky hooks + +echo "Installing husky hooks..." +husky website/.husky + +# Change current working directory to the website directory + +cd website + +# Generate tuyau types + +echo "Ensuring .env file exists..." +if [ ! -f ".env" ]; then + cp .env.example .env + node ace generate:key +fi + +echo "Generating Tuyau types..." +node ace tuyau:generate + +# Preparation is complete + +echo "ENEI Website workspace is ready!" diff --git a/website/start/routes.ts b/website/start/routes.ts index a568292..51b9fb3 100644 --- a/website/start/routes.ts +++ b/website/start/routes.ts @@ -7,4 +7,9 @@ | */ import router from '@adonisjs/core/services/router' + +const TicketsController = () => import('#controllers/tickets_controller') + router.on('/').renderInertia('home') +router.get('/tickets', [TicketsController, 'index']) +router.on('/tickets/:id/checkout').renderInertia('payments').as('checkout')