Skip to main content

Places controller for Visitors, including localization

Commit Message

Places controller for Visitors, including localization

If the i18n text is coming through in English when it should
be Spanish, make sure you have added `es` to the locales map
exported by api/src/conf/locales/index.ts and that you have
updated `supportedLocales`, exported by api/src/utils/i18n.ts,
to `LocalesEnumValues` (which ties back to the database level
enum of our supported locales).

If you get ambiguous Typescript errors along the way, restart
the ESLint server in your editor, and if that does not work,
quit and re-open your editor

The Visitor namespace uses MaybeAuthedController as its base,
so the Places index and show endpoints work whether or not the
caller is authenticated. The spec demonstrates both contexts.

```console
pnpm psy g:controller --help
pnpm psy g:controller Visitor/V1/Places index show

pnpm psy sync

pnpm build
pnpm build:spec

pnpm uspec spec/unit/controllers/Visitor/V1/PlacesController.spec.ts
// If you get 400, use NODE_DEBUG=psychic to see OpenAPI validation errors
NODE_DEBUG=psychic pnpm uspec spec/unit/controllers/Visitor/V1/PlacesController.spec.ts
pnpm uspec
```

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015KTzZW1VMPnC3xMwePsEPH

Changes

diff --git a/api/spec/unit/controllers/Visitor/V1/PlacesController.spec.ts b/api/spec/unit/controllers/Visitor/V1/PlacesController.spec.ts
new file mode 100644
index 0000000..37b19cc
--- /dev/null
+++ b/api/spec/unit/controllers/Visitor/V1/PlacesController.spec.ts
@@ -0,0 +1,223 @@
+import Place from '@models/Place.js'
+import User from '@models/User.js'
+import { PsychicServer } from '@rvoh/psychic'
+import { OpenapiSpecRequest } from '@rvoh/psychic-spec-helpers'
+import createLocalizedText from '@spec/factories/LocalizedTextFactory.js'
+import createPlace from '@spec/factories/PlaceFactory.js'
+import createBathroom from '@spec/factories/Room/BathroomFactory.js'
+import createBedroom from '@spec/factories/Room/BedroomFactory.js'
+import createDen from '@spec/factories/Room/DenFactory.js'
+import createKitchen from '@spec/factories/Room/KitchenFactory.js'
+import createLivingRoom from '@spec/factories/Room/LivingRoomFactory.js'
+import createUser from '@spec/factories/UserFactory.js'
+import { session, SpecRequestType } from '@spec/unit/helpers/authentication.js'
+import { paths as OpenapiPaths } from '@src/types/openapi/tests.openapi.js'
+
+describe('Visitor/V1/PlacesController', () => {
+ describe('authenticated', () => {
+ let request: SpecRequestType
+ let user: User
+
+ beforeEach(async () => {
+ user = await createUser()
+ request = await session(user)
+ })
+
+ describe('GET index', () => {
+ const subject = async <StatusCode extends 200 | 400>(expectedStatus: StatusCode) => {
+ return request.get('/v1/visitor/places', expectedStatus, {
+ headers: {
+ 'accept-language': 'es-ES',
+ },
+ })
+ }
+
+ it('returns the index of Places', async () => {
+ const place = await createPlace()
+ await createLocalizedText({ localizable: place, locale: 'es-ES', title: 'The Spanish title' })
+
+ const { body } = await subject(200)
+
+ expect(body.results).toEqual([
+ {
+ id: place.id,
+ title: 'The Spanish title',
+ },
+ ])
+ })
+ })
+
+ describe('GET show', () => {
+ const subject = async <StatusCode extends 200 | 400>(place: Place, expectedStatus: StatusCode) => {
+ return request.get('/v1/visitor/places/{id}', expectedStatus, {
+ id: place.id,
+ headers: {
+ 'accept-language': 'es-ES',
+ },
+ })
+ }
+
+ it('returns places with rooms', async () => {
+ const place = await createPlace({ style: 'cabin', sleeps: 3 })
+ await createLocalizedText({ localizable: place, locale: 'es-ES', title: 'The Spanish place title' })
+
+ const { kitchen, bathroom, bedroom, den, livingRoom } = await createRoomsForPlace(place)
+
+ const { body } = await subject(place, 200)
+
+ expect(body).toEqual(expectedShowBody({ place, kitchen, bathroom, bedroom, den, livingRoom }))
+ })
+ })
+ })
+
+ describe('unauthenticated', () => {
+ let request: OpenapiSpecRequest<OpenapiPaths>
+
+ beforeEach(async () => {
+ request = new OpenapiSpecRequest<OpenapiPaths>()
+ await request.init(PsychicServer)
+ })
+
+ describe('GET index', () => {
+ const subject = async <StatusCode extends 200 | 400>(expectedStatus: StatusCode) => {
+ return request.get('/v1/visitor/places', expectedStatus, {
+ headers: {
+ 'accept-language': 'es-ES',
+ },
+ })
+ }
+
+ it('returns the index of Places', async () => {
+ const place = await createPlace()
+ await createLocalizedText({ localizable: place, locale: 'es-ES', title: 'The Spanish title' })
+
+ const { body } = await subject(200)
+
+ expect(body.results).toEqual([
+ {
+ id: place.id,
+ title: 'The Spanish title',
+ },
+ ])
+ })
+ })
+
+ describe('GET show', () => {
+ const subject = async <StatusCode extends 200 | 400>(place: Place, expectedStatus: StatusCode) => {
+ return request.get('/v1/visitor/places/{id}', expectedStatus, {
+ id: place.id,
+ headers: {
+ 'accept-language': 'es-ES',
+ },
+ })
+ }
+
+ it('returns places with rooms', async () => {
+ const place = await createPlace({ style: 'cabin', sleeps: 3 })
+ await createLocalizedText({ localizable: place, locale: 'es-ES', title: 'The Spanish place title' })
+
+ const { kitchen, bathroom, bedroom, den, livingRoom } = await createRoomsForPlace(place)
+
+ const { body } = await subject(place, 200)
+
+ expect(body).toEqual(expectedShowBody({ place, kitchen, bathroom, bedroom, den, livingRoom }))
+ })
+ })
+ })
+})
+
+async function createRoomsForPlace(place: Place) {
+ const kitchen = await createKitchen({ place, appliances: ['oven', 'dishwasher'] })
+ await createLocalizedText({ localizable: kitchen, locale: 'es-ES', title: 'The Spanish kitchen title' })
+
+ const bathroom = await createBathroom({ place, bathOrShowerStyle: 'shower' })
+ await createLocalizedText({
+ localizable: bathroom,
+ locale: 'es-ES',
+ title: 'The Spanish bathroom title',
+ })
+
+ const bedroom = await createBedroom({ place, bedTypes: ['cot', 'bunk'] })
+ await createLocalizedText({ localizable: bedroom, locale: 'es-ES', title: 'The Spanish bedroom title' })
+
+ const den = await createDen({ place })
+ await createLocalizedText({ localizable: den, locale: 'es-ES', title: 'The Spanish den title' })
+
+ const livingRoom = await createLivingRoom({ place })
+ await createLocalizedText({
+ localizable: livingRoom,
+ locale: 'es-ES',
+ title: 'The Spanish livingRoom title',
+ })
+
+ return { kitchen, bathroom, bedroom, den, livingRoom }
+}
+
+function expectedShowBody({
+ place,
+ kitchen,
+ bathroom,
+ bedroom,
+ den,
+ livingRoom,
+}: Awaited<ReturnType<typeof createRoomsForPlace>> & { place: Place }) {
+ return {
+ id: place.id,
+ sleeps: 3,
+ title: 'The Spanish place title',
+ style: 'cabin',
+ displayStyle: 'cabaña rústica',
+
+ rooms: [
+ {
+ id: kitchen.id,
+ type: 'Kitchen',
+ displayType: 'cocina',
+ title: 'The Spanish kitchen title',
+ appliances: [
+ {
+ value: 'oven',
+ label: 'horno',
+ },
+ {
+ value: 'dishwasher',
+ label: 'lavavajillas',
+ },
+ ],
+ },
+ {
+ id: bathroom.id,
+ type: 'Bathroom',
+ displayType: 'baño',
+ title: 'The Spanish bathroom title',
+ bathOrShowerStyle: {
+ value: 'shower',
+ label: 'ducha',
+ },
+ },
+ {
+ id: bedroom.id,
+ type: 'Bedroom',
+ displayType: 'dormitorio',
+ title: 'The Spanish bedroom title',
+ bedTypes: [
+ {
+ value: 'cot',
+ label: 'catre',
+ },
+ {
+ value: 'bunk',
+ label: 'litera',
+ },
+ ],
+ },
+ { id: den.id, type: 'Den', displayType: 'estudio', title: 'The Spanish den title' },
+ {
+ id: livingRoom.id,
+ type: 'LivingRoom',
+ displayType: 'sala de estar',
+ title: 'The Spanish livingRoom title',
+ },
+ ],
+ }
+}
diff --git a/api/src/app/controllers/ApplicationController.ts b/api/src/app/controllers/ApplicationController.ts
index db258f4..2daa964 100644
--- a/api/src/app/controllers/ApplicationController.ts
+++ b/api/src/app/controllers/ApplicationController.ts
@@ -1,6 +1,7 @@
-import { PsychicController } from '@rvoh/psychic'
+import { BeforeAction, PsychicController } from '@rvoh/psychic'
import { PsychicOpenapiNames } from '@rvoh/psychic/openapi'
import psychicTypes from '@src/types/psychic.js'
+import { supportedLocales } from '@src/utils/i18n.js'

export default class ApplicationController extends PsychicController {
public override get psychicTypes() {
@@ -10,4 +11,17 @@ export default class ApplicationController extends PsychicController {
public static override get openapiNames(): PsychicOpenapiNames<ApplicationController> {
return ['default', 'mobile', 'tests']
}
+
+ @BeforeAction()
+ public configureSerializers() {
+ this.serializerPassthrough({
+ locale: this.locale,
+ })
+ }
+
+ protected get locale() {
+ const locale = this.header('Accept-Language')
+ const locales = supportedLocales()
+ return locales.includes(locale as (typeof locales)[number]) ? locale : 'en-US'
+ }
}
diff --git a/api/src/app/controllers/MaybeAuthedController.ts b/api/src/app/controllers/MaybeAuthedController.ts
index 5d7cb87..a36effb 100644
--- a/api/src/app/controllers/MaybeAuthedController.ts
+++ b/api/src/app/controllers/MaybeAuthedController.ts
@@ -1,16 +1,13 @@
import ApplicationController from '@controllers/ApplicationController.js'
import resolveCurrentUser from '@controllers/helpers/resolveCurrentUser.js'
+import User from '@models/User.js'
import { BeforeAction } from '@rvoh/psychic'
-/** uncomment after creating User model */
-// import User from '@models/User.js'

export default class MaybeAuthedController extends ApplicationController {
- /** uncomment after creating User model */
- // protected currentUser: User | null = null
+ protected currentUser: User | null = null

@BeforeAction()
protected async authenticate() {
- /** uncomment after creating User model */
- // this.currentUser = await resolveCurrentUser(this)
+ this.currentUser = await resolveCurrentUser(this)
}
}
diff --git a/api/src/app/controllers/Visitor/BaseController.ts b/api/src/app/controllers/Visitor/BaseController.ts
new file mode 100644
index 0000000..ee11816
--- /dev/null
+++ b/api/src/app/controllers/Visitor/BaseController.ts
@@ -0,0 +1,3 @@
+import MaybeAuthedController from '../MaybeAuthedController.js'
+
+export default class VisitorBaseController extends MaybeAuthedController {}
diff --git a/api/src/app/controllers/Visitor/V1/BaseController.ts b/api/src/app/controllers/Visitor/V1/BaseController.ts
new file mode 100644
index 0000000..e07512e
--- /dev/null
+++ b/api/src/app/controllers/Visitor/V1/BaseController.ts
@@ -0,0 +1,3 @@
+import VisitorBaseController from '../BaseController.js'
+
+export default class VisitorV1BaseController extends VisitorBaseController {}
diff --git a/api/src/app/controllers/Visitor/V1/PlacesController.ts b/api/src/app/controllers/Visitor/V1/PlacesController.ts
new file mode 100644
index 0000000..8e1044f
--- /dev/null
+++ b/api/src/app/controllers/Visitor/V1/PlacesController.ts
@@ -0,0 +1,37 @@
+import Place from '@models/Place.js'
+import { OpenAPI } from '@rvoh/psychic'
+import VisitorV1BaseController from './BaseController.js'
+
+const openApiTags = ['visitor-places']
+
+export default class VisitorV1PlacesController extends VisitorV1BaseController {
+ @OpenAPI(Place, {
+ status: 200,
+ tags: openApiTags,
+ description: 'Place index endpoint for Visitors',
+ cursorPaginate: true,
+ serializerKey: 'summaryForVisitors',
+ fastJsonStringify: true,
+ })
+ public async index() {
+ const places = await Place.passthrough({ locale: this.locale })
+ .preloadFor('summaryForVisitors')
+ .cursorPaginate({ cursor: this.castParam('cursor', 'string', { allowNull: true }) })
+ this.ok(places)
+ }
+
+ @OpenAPI(Place, {
+ status: 200,
+ tags: openApiTags,
+ description: 'Place show endpoint for Visitors',
+ serializerKey: 'forVisitors',
+ fastJsonStringify: true,
+ })
+ public async show() {
+ this.ok(
+ await Place.passthrough({ locale: this.locale })
+ .preloadFor('forVisitors')
+ .findOrFail(this.castParam('id', 'uuid')),
+ )
+ }
+}
diff --git a/api/src/app/controllers/helpers/resolveCurrentUser.ts b/api/src/app/controllers/helpers/resolveCurrentUser.ts
index 24084aa..6e5f1b1 100644
--- a/api/src/app/controllers/helpers/resolveCurrentUser.ts
+++ b/api/src/app/controllers/helpers/resolveCurrentUser.ts
@@ -10,6 +10,7 @@ export default async function resolveCurrentUser(controller: PsychicController):
)

const token = (controller.header('authorization') ?? '').split(' ').at(-1)!
+ if (!token) return null

const decrypted = Encrypt.decrypt(token, {
algorithm: 'aes-256-gcm',
diff --git a/api/src/app/models/Place.ts b/api/src/app/models/Place.ts
index a1be52d..4d3cb29 100644
--- a/api/src/app/models/Place.ts
+++ b/api/src/app/models/Place.ts
@@ -18,6 +18,8 @@ export default class Place extends ApplicationModel {
return {
default: 'PlaceSerializer',
summary: 'PlaceSummarySerializer',
+ summaryForVisitors: 'PlaceSummaryForVisitorsSerializer',
+ forVisitors: 'PlaceForVisitorsSerializer',
}
}

diff --git a/api/src/app/models/Room/Bathroom.ts b/api/src/app/models/Room/Bathroom.ts
index ae1bc18..c169467 100644
--- a/api/src/app/models/Room/Bathroom.ts
+++ b/api/src/app/models/Room/Bathroom.ts
@@ -12,6 +12,7 @@ export default class Bathroom extends Room {
return {
default: 'Room/BathroomSerializer',
summary: 'Room/BathroomSummarySerializer',
+ forVisitors: 'Room/BathroomForVisitorsSerializer',
}
}

diff --git a/api/src/app/models/Room/Bedroom.ts b/api/src/app/models/Room/Bedroom.ts
index ed0eeb4..eb3883a 100644
--- a/api/src/app/models/Room/Bedroom.ts
+++ b/api/src/app/models/Room/Bedroom.ts
@@ -12,6 +12,7 @@ export default class Bedroom extends Room {
return {
default: 'Room/BedroomSerializer',
summary: 'Room/BedroomSummarySerializer',
+ forVisitors: 'Room/BedroomForVisitorsSerializer',
}
}

diff --git a/api/src/app/models/Room/Den.ts b/api/src/app/models/Room/Den.ts
index 2c0a986..37549c0 100644
--- a/api/src/app/models/Room/Den.ts
+++ b/api/src/app/models/Room/Den.ts
@@ -12,6 +12,7 @@ export default class Den extends Room {
return {
default: 'Room/DenSerializer',
summary: 'Room/DenSummarySerializer',
+ forVisitors: 'Room/DenForVisitorsSerializer',
}
}

diff --git a/api/src/app/models/Room/Kitchen.ts b/api/src/app/models/Room/Kitchen.ts
index a38b947..671a3a9 100644
--- a/api/src/app/models/Room/Kitchen.ts
+++ b/api/src/app/models/Room/Kitchen.ts
@@ -12,6 +12,7 @@ export default class Kitchen extends Room {
return {
default: 'Room/KitchenSerializer',
summary: 'Room/KitchenSummarySerializer',
+ forVisitors: 'Room/KitchenForVisitorsSerializer',
}
}

diff --git a/api/src/app/models/Room/LivingRoom.ts b/api/src/app/models/Room/LivingRoom.ts
index 1069ae8..b6fe2b3 100644
--- a/api/src/app/models/Room/LivingRoom.ts
+++ b/api/src/app/models/Room/LivingRoom.ts
@@ -12,6 +12,7 @@ export default class LivingRoom extends Room {
return {
default: 'Room/LivingRoomSerializer',
summary: 'Room/LivingRoomSummarySerializer',
+ forVisitors: 'Room/LivingRoomForVisitorsSerializer',
}
}

diff --git a/api/src/app/serializers/PlaceSerializer.ts b/api/src/app/serializers/PlaceSerializer.ts
index bc76022..2f78daa 100644
--- a/api/src/app/serializers/PlaceSerializer.ts
+++ b/api/src/app/serializers/PlaceSerializer.ts
@@ -1,5 +1,7 @@
import { DreamSerializer } from '@rvoh/dream'
import Place from '@models/Place.js'
+import { type LocalesEnum } from '@src/types/db.js'
+import i18n from '@src/utils/i18n.js'

export const PlaceSummarySerializer = (place: Place) =>
DreamSerializer(Place, place)
@@ -10,3 +12,17 @@ export const PlaceSerializer = (place: Place) =>
PlaceSummarySerializer(place)
.attribute('style')
.attribute('sleeps')
+
+export const PlaceSummaryForVisitorsSerializer = (place: Place) =>
+ DreamSerializer(Place, place)
+ .attribute('id')
+ .delegatedAttribute('currentLocalizedText', 'title', { openapi: 'string' })
+
+export const PlaceForVisitorsSerializer = (place: Place, passthrough: { locale: LocalesEnum }) =>
+ PlaceSummaryForVisitorsSerializer(place)
+ .attribute('style')
+ .customAttribute('displayStyle', () => i18n(passthrough.locale, `places.style.${place.style}`), {
+ openapi: 'string',
+ })
+ .attribute('sleeps')
+ .rendersMany('rooms', { serializerKey: 'forVisitors' })
diff --git a/api/src/app/serializers/Room/BathroomSerializer.ts b/api/src/app/serializers/Room/BathroomSerializer.ts
index a80452b..1ea2b6f 100644
--- a/api/src/app/serializers/Room/BathroomSerializer.ts
+++ b/api/src/app/serializers/Room/BathroomSerializer.ts
@@ -1,5 +1,12 @@
-import { RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
+import { ObjectSerializer } from '@rvoh/dream'
+import { RoomForVisitorsSerializer, RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
import Bathroom from '@models/Room/Bathroom.js'
+import {
+ type BathOrShowerStylesEnum,
+ BathOrShowerStylesEnumValues,
+ type LocalesEnum,
+} from '@src/types/db.js'
+import i18n from '@src/utils/i18n.js'

export const RoomBathroomSummarySerializer = (bathroom: Bathroom) =>
RoomSummarySerializer(Bathroom, bathroom)
@@ -7,3 +14,24 @@ export const RoomBathroomSummarySerializer = (bathroom: Bathroom) =>
export const RoomBathroomSerializer = (bathroom: Bathroom) =>
RoomSerializer(Bathroom, bathroom)
.attribute('bathOrShowerStyle')
+
+export const BathOrShowerStyleSerializer = (
+ bathOrShowerStyle: BathOrShowerStylesEnum,
+ passthrough: { locale: LocalesEnum }
+) =>
+ ObjectSerializer({ bathOrShowerStyle }, passthrough)
+ .attribute('bathOrShowerStyle', {
+ as: 'value',
+ openapi: { type: 'string', enum: BathOrShowerStylesEnumValues },
+ })
+ .customAttribute(
+ 'label',
+ () => i18n(passthrough.locale, `rooms.Bathroom.bathOrShowerStyles.${bathOrShowerStyle}`),
+ {
+ openapi: 'string',
+ }
+ )
+
+export const RoomBathroomForVisitorsSerializer = (roomBathroom: Bathroom, passthrough: { locale: LocalesEnum }) =>
+ RoomForVisitorsSerializer(Bathroom, roomBathroom, passthrough)
+ .rendersOne<Bathroom>('bathOrShowerStyle', { serializer: BathOrShowerStyleSerializer })
diff --git a/api/src/app/serializers/Room/BedroomSerializer.ts b/api/src/app/serializers/Room/BedroomSerializer.ts
index f34b5b4..451df83 100644
--- a/api/src/app/serializers/Room/BedroomSerializer.ts
+++ b/api/src/app/serializers/Room/BedroomSerializer.ts
@@ -1,5 +1,8 @@
-import { RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
+import { ObjectSerializer } from '@rvoh/dream'
+import { RoomForVisitorsSerializer, RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
import Bedroom from '@models/Room/Bedroom.js'
+import { type BedTypesEnum, BedTypesEnumValues, type LocalesEnum } from '@src/types/db.js'
+import i18n from '@src/utils/i18n.js'

export const RoomBedroomSummarySerializer = (bedroom: Bedroom) =>
RoomSummarySerializer(Bedroom, bedroom)
@@ -7,3 +10,15 @@ export const RoomBedroomSummarySerializer = (bedroom: Bedroom) =>
export const RoomBedroomSerializer = (bedroom: Bedroom) =>
RoomSerializer(Bedroom, bedroom)
.attribute('bedTypes')
+
+export const BedTypeSerializer = (bedType: BedTypesEnum, passthrough: { locale: LocalesEnum }) =>
+ ObjectSerializer({ bedType }, passthrough)
+ .attribute('bedType', { as: 'value', openapi: { type: 'string', enum: BedTypesEnumValues } })
+ .customAttribute('label', () => i18n(passthrough.locale, `rooms.Bedroom.bedTypes.${bedType}`), {
+ openapi: 'string',
+ })
+
+export const RoomBedroomForVisitorsSerializer = (roomBedroom: Bedroom, passthrough: { locale: LocalesEnum }) =>
+ RoomForVisitorsSerializer(Bedroom, roomBedroom, passthrough).rendersMany<Bedroom>('bedTypes', {
+ serializer: BedTypeSerializer,
+ })
diff --git a/api/src/app/serializers/Room/DenSerializer.ts b/api/src/app/serializers/Room/DenSerializer.ts
index 5d4db09..6a44070 100644
--- a/api/src/app/serializers/Room/DenSerializer.ts
+++ b/api/src/app/serializers/Room/DenSerializer.ts
@@ -1,8 +1,12 @@
-import { RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
+import { RoomForVisitorsSerializer, RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
import Den from '@models/Room/Den.js'
+import { type LocalesEnum } from '@src/types/db.js'

export const RoomDenSummarySerializer = (den: Den) =>
RoomSummarySerializer(Den, den)

export const RoomDenSerializer = (den: Den) =>
RoomSerializer(Den, den)
+
+export const RoomDenForVisitorsSerializer = (roomDen: Den, passthrough: { locale: LocalesEnum }) =>
+ RoomForVisitorsSerializer(Den, roomDen, passthrough)
diff --git a/api/src/app/serializers/Room/KitchenSerializer.ts b/api/src/app/serializers/Room/KitchenSerializer.ts
index 633aa8f..5f2929c 100644
--- a/api/src/app/serializers/Room/KitchenSerializer.ts
+++ b/api/src/app/serializers/Room/KitchenSerializer.ts
@@ -1,5 +1,8 @@
-import { RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
+import { ObjectSerializer } from '@rvoh/dream'
+import { RoomForVisitorsSerializer, RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
import Kitchen from '@models/Room/Kitchen.js'
+import { type ApplianceTypesEnum, ApplianceTypesEnumValues, type LocalesEnum } from '@src/types/db.js'
+import i18n from '@src/utils/i18n.js'

export const RoomKitchenSummarySerializer = (kitchen: Kitchen) =>
RoomSummarySerializer(Kitchen, kitchen)
@@ -7,3 +10,15 @@ export const RoomKitchenSummarySerializer = (kitchen: Kitchen) =>
export const RoomKitchenSerializer = (kitchen: Kitchen) =>
RoomSerializer(Kitchen, kitchen)
.attribute('appliances')
+
+export const ApplianceSerializer = (appliance: ApplianceTypesEnum, passthrough: { locale: LocalesEnum }) =>
+ ObjectSerializer({ appliance }, passthrough)
+ .attribute('appliance', { as: 'value', openapi: { type: 'string', enum: ApplianceTypesEnumValues } })
+ .customAttribute('label', () => i18n(passthrough.locale, `rooms.Kitchen.appliances.${appliance}`), {
+ openapi: 'string',
+ })
+
+export const RoomKitchenForVisitorsSerializer = (roomKitchen: Kitchen, passthrough: { locale: LocalesEnum }) =>
+ RoomForVisitorsSerializer(Kitchen, roomKitchen, passthrough).rendersMany<Kitchen>('appliances', {
+ serializer: ApplianceSerializer,
+ })
diff --git a/api/src/app/serializers/Room/LivingRoomSerializer.ts b/api/src/app/serializers/Room/LivingRoomSerializer.ts
index 473d39b..1c17f34 100644
--- a/api/src/app/serializers/Room/LivingRoomSerializer.ts
+++ b/api/src/app/serializers/Room/LivingRoomSerializer.ts
@@ -1,8 +1,14 @@
-import { RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
+import { RoomForVisitorsSerializer, RoomSerializer, RoomSummarySerializer } from '@serializers/RoomSerializer.js'
import LivingRoom from '@models/Room/LivingRoom.js'
+import { type LocalesEnum } from '@src/types/db.js'

export const RoomLivingRoomSummarySerializer = (livingRoom: LivingRoom) =>
RoomSummarySerializer(LivingRoom, livingRoom)

export const RoomLivingRoomSerializer = (livingRoom: LivingRoom) =>
RoomSerializer(LivingRoom, livingRoom)
+
+export const RoomLivingRoomForVisitorsSerializer = (
+ roomLivingRoom: LivingRoom,
+ passthrough: { locale: LocalesEnum }
+) => RoomForVisitorsSerializer(LivingRoom, roomLivingRoom, passthrough)
diff --git a/api/src/app/serializers/RoomSerializer.ts b/api/src/app/serializers/RoomSerializer.ts
index d16e185..5292ce7 100644
--- a/api/src/app/serializers/RoomSerializer.ts
+++ b/api/src/app/serializers/RoomSerializer.ts
@@ -1,5 +1,7 @@
import { DreamSerializer } from '@rvoh/dream'
import Room from '@models/Room.js'
+import { type LocalesEnum } from '@src/types/db.js'
+import i18n from '@src/utils/i18n.js'

export const RoomSummarySerializer = <T extends Room>(StiChildClass: typeof Room, room: T) =>
DreamSerializer(StiChildClass ?? Room, room)
@@ -9,3 +11,16 @@ export const RoomSummarySerializer = <T extends Room>(StiChildClass: typeof Room

export const RoomSerializer = <T extends Room>(StiChildClass: typeof Room, room: T) =>
RoomSummarySerializer(StiChildClass, room)
+
+export const RoomForVisitorsSerializer = <T extends Room>(
+ StiChildClass: typeof Room,
+ room: T,
+ passthrough: { locale: LocalesEnum }
+) =>
+ DreamSerializer(StiChildClass ?? Room, room)
+ .attribute('id')
+ .attribute('type', { openapi: { type: 'string', enum: [(StiChildClass ?? Room).sanitizedName] } })
+ .customAttribute('displayType', () => i18n(passthrough.locale, `rooms.type.${room.type}`), {
+ openapi: 'string',
+ })
+ .delegatedAttribute<Room, 'currentLocalizedText'>('currentLocalizedText', 'title', { openapi: 'string' })
diff --git a/api/src/conf/locales/en.ts b/api/src/conf/locales/en.ts
index ae21584..1e720ed 100644
--- a/api/src/conf/locales/en.ts
+++ b/api/src/conf/locales/en.ts
@@ -1,3 +1,52 @@
export default {
- // add your application's localizable text in here
+ places: {
+ style: {
+ cottage: 'cottage',
+ cabin: 'cabin',
+ lean_to: 'lean to',
+ treehouse: 'treehouse',
+ tent: 'tent',
+ cave: 'cave',
+ dump: 'dump',
+ },
+ },
+
+ rooms: {
+ type: {
+ Bathroom: 'bathroom',
+ Bedroom: 'bedroom',
+ Kitchen: 'kitchen',
+ Den: 'den',
+ LivingRoom: 'living room',
+ },
+
+ Bathroom: {
+ bathOrShowerStyles: {
+ bath: 'bath',
+ shower: 'shower',
+ bath_and_shower: 'bath and shower',
+ none: 'none',
+ },
+ },
+
+ Bedroom: {
+ bedTypes: {
+ twin: 'twin',
+ bunk: 'bunk',
+ queen: 'queen',
+ king: 'king',
+ cot: 'cot',
+ sofabed: 'sofabed',
+ },
+ },
+
+ Kitchen: {
+ appliances: {
+ stove: 'stove',
+ oven: 'oven',
+ microwave: 'microwave',
+ dishwasher: 'dishwasher',
+ },
+ },
+ },
}
diff --git a/api/src/conf/locales/es.ts b/api/src/conf/locales/es.ts
new file mode 100644
index 0000000..25eb876
--- /dev/null
+++ b/api/src/conf/locales/es.ts
@@ -0,0 +1,52 @@
+export default {
+ places: {
+ style: {
+ cottage: 'cabaña',
+ cabin: 'cabaña rústica',
+ lean_to: 'refugio',
+ treehouse: 'casa del árbol',
+ tent: 'tienda de campaña',
+ cave: 'cueva',
+ dump: 'basurero',
+ },
+ },
+
+ rooms: {
+ type: {
+ Bathroom: 'baño',
+ Bedroom: 'dormitorio',
+ Kitchen: 'cocina',
+ Den: 'estudio',
+ LivingRoom: 'sala de estar',
+ },
+
+ Bathroom: {
+ bathOrShowerStyles: {
+ bath: 'bañera',
+ shower: 'ducha',
+ bath_and_shower: 'bañera y ducha',
+ none: 'ninguno',
+ },
+ },
+
+ Bedroom: {
+ bedTypes: {
+ twin: 'individual',
+ bunk: 'litera',
+ queen: 'matrimonial',
+ king: 'king',
+ cot: 'catre',
+ sofabed: 'sofá cama',
+ },
+ },
+
+ Kitchen: {
+ appliances: {
+ stove: 'estufa',
+ oven: 'horno',
+ microwave: 'microondas',
+ dishwasher: 'lavavajillas',
+ },
+ },
+ },
+}
diff --git a/api/src/conf/locales/index.ts b/api/src/conf/locales/index.ts
index b798cf2..82b076e 100644
--- a/api/src/conf/locales/index.ts
+++ b/api/src/conf/locales/index.ts
@@ -1,5 +1,7 @@
import en from '@conf/locales/en.js'
+import es from '@conf/locales/es.js'

export default {
en,
+ es,
}
diff --git a/api/src/conf/routes.ts b/api/src/conf/routes.ts
index ff6749b..e4182bb 100644
--- a/api/src/conf/routes.ts
+++ b/api/src/conf/routes.ts
@@ -1,17 +1,21 @@
import adminRoutes from '@conf/routes.admin.js'
import internalRoutes from '@conf/routes.internal.js'
+import VisitorPlacesController from '@controllers/Visitor/V1/PlacesController.js'
import { PsychicRouter } from '@rvoh/psychic'

export default function routes(r: PsychicRouter) {
r.namespace('v1', r => {
+ r.namespace('visitor', r => {
+ r.get('places', VisitorPlacesController, 'index')
+ r.get('places/:id', VisitorPlacesController, 'show')
+ })
+
r.namespace('host', r => {
r.resources('localized-texts', { only: ['update', 'destroy'] })

r.resources('places', r => {
r.resources('rooms')
-
})
-
})
})

diff --git a/api/src/openapi/mobile.openapi.json b/api/src/openapi/mobile.openapi.json
index 9cf5e7d..31ca0e5 100644
--- a/api/src/openapi/mobile.openapi.json
+++ b/api/src/openapi/mobile.openapi.json
@@ -781,10 +781,177 @@
}
}
}
+ },
+ "/v1/visitor/places": {
+ "parameters": [
+ {
+ "in": "query",
+ "required": false,
+ "name": "cursor",
+ "description": "Pagination cursor",
+ "allowReserved": true,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "visitor-places"
+ ],
+ "description": "Place index endpoint for Visitors",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "cursor",
+ "results"
+ ],
+ "properties": {
+ "cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PlaceSummaryForVisitors"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
+ },
+ "/v1/visitor/places/{id}": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "visitor-places"
+ ],
+ "description": "Place show endpoint for Visitors",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PlaceForVisitors"
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
}
},
"components": {
"schemas": {
+ "Appliance": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "description": "\nThe following values will be allowed:\n dishwasher,\n microwave,\n oven,\n stove"
+ }
+ }
+ },
+ "BathOrShowerStyle": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "description": "\nThe following values will be allowed:\n bath,\n bath_and_shower,\n none,\n shower"
+ }
+ }
+ },
+ "BedType": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "description": "\nThe following values will be allowed:\n bunk,\n cot,\n king,\n queen,\n sofabed,\n twin"
+ }
+ }
+ },
"OpenapiValidationErrors": {
"type": "object",
"required": [
@@ -864,6 +1031,57 @@
}
}
},
+ "PlaceForVisitors": {
+ "type": "object",
+ "required": [
+ "displayStyle",
+ "id",
+ "rooms",
+ "sleeps",
+ "style",
+ "title"
+ ],
+ "properties": {
+ "displayStyle": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "rooms": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/RoomBathroomForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomBedroomForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomDenForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomKitchenForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomLivingRoomForVisitors"
+ }
+ ]
+ }
+ },
+ "sleeps": {
+ "type": "integer"
+ },
+ "style": {
+ "type": "string",
+ "description": "The following values will be allowed:\n cabin,\n cave,\n cottage,\n dump,\n lean_to,\n tent,\n treehouse"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"PlaceSummary": {
"type": "object",
"required": [
@@ -879,6 +1097,21 @@
}
}
},
+ "PlaceSummaryForVisitors": {
+ "type": "object",
+ "required": [
+ "id",
+ "title"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"RoomBathroom": {
"type": "object",
"required": [
@@ -910,6 +1143,34 @@
}
}
},
+ "RoomBathroomForVisitors": {
+ "type": "object",
+ "required": [
+ "bathOrShowerStyle",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "bathOrShowerStyle": {
+ "$ref": "#/components/schemas/BathOrShowerStyle"
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "description": "The following values will be allowed:\n Bathroom"
+ }
+ }
+ },
"RoomBathroomSummary": {
"type": "object",
"required": [
@@ -964,6 +1225,37 @@
}
}
},
+ "RoomBedroomForVisitors": {
+ "type": "object",
+ "required": [
+ "bedTypes",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BedType"
+ }
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "description": "The following values will be allowed:\n Bedroom"
+ }
+ }
+ },
"RoomBedroomSummary": {
"type": "object",
"required": [
@@ -1010,6 +1302,30 @@
}
}
},
+ "RoomDenForVisitors": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "description": "The following values will be allowed:\n Den"
+ }
+ }
+ },
"RoomDenSummary": {
"type": "object",
"required": [
@@ -1064,6 +1380,37 @@
}
}
},
+ "RoomKitchenForVisitors": {
+ "type": "object",
+ "required": [
+ "appliances",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Appliance"
+ }
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "description": "The following values will be allowed:\n Kitchen"
+ }
+ }
+ },
"RoomKitchenSummary": {
"type": "object",
"required": [
@@ -1110,6 +1457,30 @@
}
}
},
+ "RoomLivingRoomForVisitors": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "description": "The following values will be allowed:\n LivingRoom"
+ }
+ }
+ },
"RoomLivingRoomSummary": {
"type": "object",
"required": [
diff --git a/api/src/openapi/openapi.json b/api/src/openapi/openapi.json
index 5cc1512..8e7f7cf 100644
--- a/api/src/openapi/openapi.json
+++ b/api/src/openapi/openapi.json
@@ -781,10 +781,194 @@
}
}
}
+ },
+ "/v1/visitor/places": {
+ "parameters": [
+ {
+ "in": "query",
+ "required": false,
+ "name": "cursor",
+ "description": "Pagination cursor",
+ "allowReserved": true,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "visitor-places"
+ ],
+ "description": "Place index endpoint for Visitors",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "cursor",
+ "results"
+ ],
+ "properties": {
+ "cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PlaceSummaryForVisitors"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
+ },
+ "/v1/visitor/places/{id}": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "visitor-places"
+ ],
+ "description": "Place show endpoint for Visitors",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PlaceForVisitors"
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
}
},
"components": {
"schemas": {
+ "Appliance": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ }
+ },
+ "BathOrShowerStyle": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower"
+ ]
+ }
+ }
+ },
+ "BedType": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ }
+ },
"OpenapiValidationErrors": {
"type": "object",
"required": [
@@ -872,6 +1056,65 @@
}
}
},
+ "PlaceForVisitors": {
+ "type": "object",
+ "required": [
+ "displayStyle",
+ "id",
+ "rooms",
+ "sleeps",
+ "style",
+ "title"
+ ],
+ "properties": {
+ "displayStyle": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "rooms": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/RoomBathroomForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomBedroomForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomDenForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomKitchenForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomLivingRoomForVisitors"
+ }
+ ]
+ }
+ },
+ "sleeps": {
+ "type": "integer"
+ },
+ "style": {
+ "type": "string",
+ "enum": [
+ "cabin",
+ "cave",
+ "cottage",
+ "dump",
+ "lean_to",
+ "tent",
+ "treehouse"
+ ]
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"PlaceSummary": {
"type": "object",
"required": [
@@ -887,6 +1130,21 @@
}
}
},
+ "PlaceSummaryForVisitors": {
+ "type": "object",
+ "required": [
+ "id",
+ "title"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"RoomBathroom": {
"type": "object",
"required": [
@@ -926,6 +1184,36 @@
}
}
},
+ "RoomBathroomForVisitors": {
+ "type": "object",
+ "required": [
+ "bathOrShowerStyle",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "bathOrShowerStyle": {
+ "$ref": "#/components/schemas/BathOrShowerStyle"
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom"
+ ]
+ }
+ }
+ },
"RoomBathroomSummary": {
"type": "object",
"required": [
@@ -991,6 +1279,39 @@
}
}
},
+ "RoomBedroomForVisitors": {
+ "type": "object",
+ "required": [
+ "bedTypes",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BedType"
+ }
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bedroom"
+ ]
+ }
+ }
+ },
"RoomBedroomSummary": {
"type": "object",
"required": [
@@ -1041,6 +1362,32 @@
}
}
},
+ "RoomDenForVisitors": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Den"
+ ]
+ }
+ }
+ },
"RoomDenSummary": {
"type": "object",
"required": [
@@ -1104,6 +1451,39 @@
}
}
},
+ "RoomKitchenForVisitors": {
+ "type": "object",
+ "required": [
+ "appliances",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Appliance"
+ }
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Kitchen"
+ ]
+ }
+ }
+ },
"RoomKitchenSummary": {
"type": "object",
"required": [
@@ -1154,6 +1534,32 @@
}
}
},
+ "RoomLivingRoomForVisitors": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "LivingRoom"
+ ]
+ }
+ }
+ },
"RoomLivingRoomSummary": {
"type": "object",
"required": [
diff --git a/api/src/openapi/tests.openapi.json b/api/src/openapi/tests.openapi.json
index 02dd314..070b864 100644
--- a/api/src/openapi/tests.openapi.json
+++ b/api/src/openapi/tests.openapi.json
@@ -781,10 +781,194 @@
}
}
}
+ },
+ "/v1/visitor/places": {
+ "parameters": [
+ {
+ "in": "query",
+ "required": false,
+ "name": "cursor",
+ "description": "Pagination cursor",
+ "allowReserved": true,
+ "schema": {
+ "type": [
+ "string",
+ "null"
+ ]
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "visitor-places"
+ ],
+ "description": "Place index endpoint for Visitors",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "type": "object",
+ "required": [
+ "cursor",
+ "results"
+ ],
+ "properties": {
+ "cursor": {
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "results": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/PlaceSummaryForVisitors"
+ }
+ }
+ }
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
+ },
+ "/v1/visitor/places/{id}": {
+ "parameters": [
+ {
+ "in": "path",
+ "name": "id",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "get": {
+ "tags": [
+ "visitor-places"
+ ],
+ "description": "Place show endpoint for Visitors",
+ "responses": {
+ "200": {
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/PlaceForVisitors"
+ }
+ }
+ },
+ "description": "Success"
+ },
+ "400": {
+ "$ref": "#/components/responses/BadRequest"
+ },
+ "401": {
+ "$ref": "#/components/responses/Unauthorized"
+ },
+ "403": {
+ "$ref": "#/components/responses/Forbidden"
+ },
+ "404": {
+ "$ref": "#/components/responses/NotFound"
+ },
+ "409": {
+ "$ref": "#/components/responses/Conflict"
+ },
+ "500": {
+ "$ref": "#/components/responses/InternalServerError"
+ }
+ }
+ }
}
},
"components": {
"schemas": {
+ "Appliance": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "enum": [
+ "dishwasher",
+ "microwave",
+ "oven",
+ "stove"
+ ]
+ }
+ }
+ },
+ "BathOrShowerStyle": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "enum": [
+ "bath",
+ "bath_and_shower",
+ "none",
+ "shower"
+ ]
+ }
+ }
+ },
+ "BedType": {
+ "type": "object",
+ "required": [
+ "label",
+ "value"
+ ],
+ "properties": {
+ "label": {
+ "type": "string"
+ },
+ "value": {
+ "type": "string",
+ "enum": [
+ "bunk",
+ "cot",
+ "king",
+ "queen",
+ "sofabed",
+ "twin"
+ ]
+ }
+ }
+ },
"OpenapiValidationErrors": {
"type": "object",
"required": [
@@ -872,6 +1056,65 @@
}
}
},
+ "PlaceForVisitors": {
+ "type": "object",
+ "required": [
+ "displayStyle",
+ "id",
+ "rooms",
+ "sleeps",
+ "style",
+ "title"
+ ],
+ "properties": {
+ "displayStyle": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "rooms": {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "$ref": "#/components/schemas/RoomBathroomForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomBedroomForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomDenForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomKitchenForVisitors"
+ },
+ {
+ "$ref": "#/components/schemas/RoomLivingRoomForVisitors"
+ }
+ ]
+ }
+ },
+ "sleeps": {
+ "type": "integer"
+ },
+ "style": {
+ "type": "string",
+ "enum": [
+ "cabin",
+ "cave",
+ "cottage",
+ "dump",
+ "lean_to",
+ "tent",
+ "treehouse"
+ ]
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"PlaceSummary": {
"type": "object",
"required": [
@@ -887,6 +1130,21 @@
}
}
},
+ "PlaceSummaryForVisitors": {
+ "type": "object",
+ "required": [
+ "id",
+ "title"
+ ],
+ "properties": {
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ }
+ }
+ },
"RoomBathroom": {
"type": "object",
"required": [
@@ -926,6 +1184,36 @@
}
}
},
+ "RoomBathroomForVisitors": {
+ "type": "object",
+ "required": [
+ "bathOrShowerStyle",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "bathOrShowerStyle": {
+ "$ref": "#/components/schemas/BathOrShowerStyle"
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bathroom"
+ ]
+ }
+ }
+ },
"RoomBathroomSummary": {
"type": "object",
"required": [
@@ -991,6 +1279,39 @@
}
}
},
+ "RoomBedroomForVisitors": {
+ "type": "object",
+ "required": [
+ "bedTypes",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "bedTypes": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/BedType"
+ }
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Bedroom"
+ ]
+ }
+ }
+ },
"RoomBedroomSummary": {
"type": "object",
"required": [
@@ -1041,6 +1362,32 @@
}
}
},
+ "RoomDenForVisitors": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Den"
+ ]
+ }
+ }
+ },
"RoomDenSummary": {
"type": "object",
"required": [
@@ -1104,6 +1451,39 @@
}
}
},
+ "RoomKitchenForVisitors": {
+ "type": "object",
+ "required": [
+ "appliances",
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "appliances": {
+ "type": "array",
+ "items": {
+ "$ref": "#/components/schemas/Appliance"
+ }
+ },
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "Kitchen"
+ ]
+ }
+ }
+ },
"RoomKitchenSummary": {
"type": "object",
"required": [
@@ -1154,6 +1534,32 @@
}
}
},
+ "RoomLivingRoomForVisitors": {
+ "type": "object",
+ "required": [
+ "displayType",
+ "id",
+ "title",
+ "type"
+ ],
+ "properties": {
+ "displayType": {
+ "type": "string"
+ },
+ "id": {
+ "type": "string"
+ },
+ "title": {
+ "type": "string"
+ },
+ "type": {
+ "type": "string",
+ "enum": [
+ "LivingRoom"
+ ]
+ }
+ }
+ },
"RoomLivingRoomSummary": {
"type": "object",
"required": [
diff --git a/api/src/types/dream.globals.ts b/api/src/types/dream.globals.ts
index 0f826c1..f8f3481 100644
--- a/api/src/types/dream.globals.ts
+++ b/api/src/types/dream.globals.ts
@@ -64,18 +64,29 @@ export const globalTypeConfig = {
'HostSummarySerializer',
'LocalizedTextSerializer',
'LocalizedTextSummarySerializer',
+ 'PlaceForVisitorsSerializer',
'PlaceSerializer',
+ 'PlaceSummaryForVisitorsSerializer',
'PlaceSummarySerializer',
+ 'Room/ApplianceSerializer',
+ 'Room/BathOrShowerStyleSerializer',
+ 'Room/BathroomForVisitorsSerializer',
'Room/BathroomSerializer',
'Room/BathroomSummarySerializer',
+ 'Room/BedTypeSerializer',
+ 'Room/BedroomForVisitorsSerializer',
'Room/BedroomSerializer',
'Room/BedroomSummarySerializer',
+ 'Room/DenForVisitorsSerializer',
'Room/DenSerializer',
'Room/DenSummarySerializer',
+ 'Room/KitchenForVisitorsSerializer',
'Room/KitchenSerializer',
'Room/KitchenSummarySerializer',
+ 'Room/LivingRoomForVisitorsSerializer',
'Room/LivingRoomSerializer',
'Room/LivingRoomSummarySerializer',
+ 'RoomForVisitorsSerializer',
'RoomSerializer',
'RoomSummarySerializer',
],
diff --git a/api/src/types/dream.ts b/api/src/types/dream.ts
index d73f541..c881e17 100644
--- a/api/src/types/dream.ts
+++ b/api/src/types/dream.ts
@@ -456,7 +456,7 @@ export const schema = {
},
},
places: {
- serializerKeys: ['default', 'summary'],
+ serializerKeys: ['default', 'forVisitors', 'summary', 'summaryForVisitors'],
scopes: {
default: ['dream:SoftDelete'],
named: [],
@@ -585,7 +585,7 @@ export const schema = {
},
},
rooms: {
- serializerKeys: ['default', 'summary'],
+ serializerKeys: ['default', 'forVisitors', 'summary'],
scopes: {
default: ['dream:STI', 'dream:SoftDelete'],
named: [],
diff --git a/api/src/types/openapi/tests.openapi.d.ts b/api/src/types/openapi/tests.openapi.d.ts
index f619198..82894ca 100644
--- a/api/src/types/openapi/tests.openapi.d.ts
+++ b/api/src/types/openapi/tests.openapi.d.ts
@@ -454,10 +454,122 @@ export interface paths {
};
trace?: never;
};
+ "/v1/visitor/places": {
+ parameters: {
+ query?: {
+ /** @description Pagination cursor */
+ cursor?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ /** @description Place index endpoint for Visitors */
+ get: {
+ parameters: {
+ query?: {
+ /** @description Pagination cursor */
+ cursor?: string | null;
+ };
+ header?: never;
+ path?: never;
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Success */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": {
+ cursor: string | null;
+ results: components["schemas"]["PlaceSummaryForVisitors"][];
+ };
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
+ "/v1/visitor/places/{id}": {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ /** @description Place show endpoint for Visitors */
+ get: {
+ parameters: {
+ query?: never;
+ header?: never;
+ path: {
+ id: string;
+ };
+ cookie?: never;
+ };
+ requestBody?: never;
+ responses: {
+ /** @description Success */
+ 200: {
+ headers: {
+ [name: string]: unknown;
+ };
+ content: {
+ "application/json": components["schemas"]["PlaceForVisitors"];
+ };
+ };
+ 400: components["responses"]["BadRequest"];
+ 401: components["responses"]["Unauthorized"];
+ 403: components["responses"]["Forbidden"];
+ 404: components["responses"]["NotFound"];
+ 409: components["responses"]["Conflict"];
+ 500: components["responses"]["InternalServerError"];
+ };
+ };
+ put?: never;
+ post?: never;
+ delete?: never;
+ options?: never;
+ head?: never;
+ patch?: never;
+ trace?: never;
+ };
}
export type webhooks = Record<string, never>;
export interface components {
schemas: {
+ Appliance: {
+ label: string;
+ /** @enum {string} */
+ value: "dishwasher" | "microwave" | "oven" | "stove";
+ };
+ BathOrShowerStyle: {
+ label: string;
+ /** @enum {string} */
+ value: "bath" | "bath_and_shower" | "none" | "shower";
+ };
+ BedType: {
+ label: string;
+ /** @enum {string} */
+ value: "bunk" | "cot" | "king" | "queen" | "sofabed" | "twin";
+ };
OpenapiValidationErrors: {
/** @enum {string} */
type: "openapi";
@@ -478,10 +590,23 @@ export interface components {
/** @enum {string} */
style: "cabin" | "cave" | "cottage" | "dump" | "lean_to" | "tent" | "treehouse";
};
+ PlaceForVisitors: {
+ displayStyle: string;
+ id: string;
+ rooms: (components["schemas"]["RoomBathroomForVisitors"] | components["schemas"]["RoomBedroomForVisitors"] | components["schemas"]["RoomDenForVisitors"] | components["schemas"]["RoomKitchenForVisitors"] | components["schemas"]["RoomLivingRoomForVisitors"])[];
+ sleeps: number;
+ /** @enum {string} */
+ style: "cabin" | "cave" | "cottage" | "dump" | "lean_to" | "tent" | "treehouse";
+ title: string;
+ };
PlaceSummary: {
id: string;
name: string;
};
+ PlaceSummaryForVisitors: {
+ id: string;
+ title: string;
+ };
RoomBathroom: {
/** @enum {string|null} */
bathOrShowerStyle: "bath" | "bath_and_shower" | "none" | "shower" | null;
@@ -490,6 +615,14 @@ export interface components {
/** @enum {string} */
type: "Bathroom";
};
+ RoomBathroomForVisitors: {
+ bathOrShowerStyle: components["schemas"]["BathOrShowerStyle"];
+ displayType: string;
+ id: string;
+ title: string;
+ /** @enum {string} */
+ type: "Bathroom";
+ };
RoomBathroomSummary: {
id: string;
position: number | null;
@@ -503,6 +636,14 @@ export interface components {
/** @enum {string} */
type: "Bedroom";
};
+ RoomBedroomForVisitors: {
+ bedTypes: components["schemas"]["BedType"][];
+ displayType: string;
+ id: string;
+ title: string;
+ /** @enum {string} */
+ type: "Bedroom";
+ };
RoomBedroomSummary: {
id: string;
position: number | null;
@@ -515,6 +656,13 @@ export interface components {
/** @enum {string} */
type: "Den";
};
+ RoomDenForVisitors: {
+ displayType: string;
+ id: string;
+ title: string;
+ /** @enum {string} */
+ type: "Den";
+ };
RoomDenSummary: {
id: string;
position: number | null;
@@ -528,6 +676,14 @@ export interface components {
/** @enum {string} */
type: "Kitchen";
};
+ RoomKitchenForVisitors: {
+ appliances: components["schemas"]["Appliance"][];
+ displayType: string;
+ id: string;
+ title: string;
+ /** @enum {string} */
+ type: "Kitchen";
+ };
RoomKitchenSummary: {
id: string;
position: number | null;
@@ -540,6 +696,13 @@ export interface components {
/** @enum {string} */
type: "LivingRoom";
};
+ RoomLivingRoomForVisitors: {
+ displayType: string;
+ id: string;
+ title: string;
+ /** @enum {string} */
+ type: "LivingRoom";
+ };
RoomLivingRoomSummary: {
id: string;
position: number | null;
diff --git a/api/src/utils/i18n.ts b/api/src/utils/i18n.ts
index c99a20e..b5ec778 100644
--- a/api/src/utils/i18n.ts
+++ b/api/src/utils/i18n.ts
@@ -1,9 +1,9 @@
import locales from '@conf/locales/index.js'
import { I18nProvider } from '@rvoh/psychic/system'
+import { LocalesEnumValues } from '@src/types/db.js'

-const SUPPORTED_LOCALES = ['en-US']
export function supportedLocales() {
- return SUPPORTED_LOCALES
+ return LocalesEnumValues
}

export default I18nProvider.provide(locales, 'en')