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')