diff --git a/drizzle/0018_blushing_daimon_hellstrom.sql b/drizzle/0018_blushing_daimon_hellstrom.sql
new file mode 100644
index 00000000..60357f25
--- /dev/null
+++ b/drizzle/0018_blushing_daimon_hellstrom.sql
@@ -0,0 +1 @@
+ALTER TABLE `place` MODIFY COLUMN `featuresId` int NOT NULL;
\ No newline at end of file
diff --git a/drizzle/meta/0018_snapshot.json b/drizzle/meta/0018_snapshot.json
new file mode 100644
index 00000000..9fe15b7c
--- /dev/null
+++ b/drizzle/meta/0018_snapshot.json
@@ -0,0 +1,1014 @@
+{
+ "version": "5",
+ "dialect": "mysql",
+ "id": "d3bfad8c-c019-4c9b-a4c7-ac10c6854fc3",
+ "prevId": "8d05399e-7f45-488d-a39f-74201ee934fc",
+ "tables": {
+ "account": {
+ "name": "account",
+ "columns": {
+ "userId": {
+ "name": "userId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "type": {
+ "name": "type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "provider": {
+ "name": "provider",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "providerAccountId": {
+ "name": "providerAccountId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "refresh_token": {
+ "name": "refresh_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "access_token": {
+ "name": "access_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "expires_at": {
+ "name": "expires_at",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "token_type": {
+ "name": "token_type",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "scope": {
+ "name": "scope",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "id_token": {
+ "name": "id_token",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "session_state": {
+ "name": "session_state",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "account_provider_providerAccountId_pk": {
+ "name": "account_provider_providerAccountId_pk",
+ "columns": [
+ "provider",
+ "providerAccountId"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "session": {
+ "name": "session",
+ "columns": {
+ "sessionToken": {
+ "name": "sessionToken",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "session_sessionToken": {
+ "name": "session_sessionToken",
+ "columns": [
+ "sessionToken"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "verificationToken": {
+ "name": "verificationToken",
+ "columns": {
+ "identifier": {
+ "name": "identifier",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "token": {
+ "name": "token",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "expires": {
+ "name": "expires",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationToken_identifier_token_pk": {
+ "name": "verificationToken_identifier_token_pk",
+ "columns": [
+ "identifier",
+ "token"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "feature": {
+ "name": "feature",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "amountOfPeople": {
+ "name": "amountOfPeople",
+ "type": "enum('none','few','some','many','crowded')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "difficulty": {
+ "name": "difficulty",
+ "type": "enum('accessible','normal','smallEffort','hard','dangerous')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "groundType": {
+ "name": "groundType",
+ "type": "enum('sand','pebbles','rocks','concrete','dirt','pavimented')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasBus": {
+ "name": "hasBus",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasParking": {
+ "name": "hasParking",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "parkingSpaces": {
+ "name": "parkingSpaces",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasToilet": {
+ "name": "hasToilet",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasRestaurant": {
+ "name": "hasRestaurant",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasDrinkingWater": {
+ "name": "hasDrinkingWater",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasShower": {
+ "name": "hasShower",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasLifeguard": {
+ "name": "hasLifeguard",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hasLeisure": {
+ "name": "hasLeisure",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "dimensions": {
+ "name": "dimensions",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "price": {
+ "name": "price",
+ "type": "double",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "priceUnit": {
+ "name": "priceUnit",
+ "type": "enum('eur','eur/minute','eur/hour','eur/day')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "isCovered": {
+ "name": "isCovered",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "timeToArrive": {
+ "name": "timeToArrive",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "placeToArriveFrom": {
+ "name": "placeToArriveFrom",
+ "type": "enum('townCenter','parking','beach','road')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "isFreeWithLocalStamp": {
+ "name": "isFreeWithLocalStamp",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "difficultyNotes": {
+ "name": "difficultyNotes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "priceNotes": {
+ "name": "priceNotes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "feature_id": {
+ "name": "feature_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "feature_translation": {
+ "name": "feature_translation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "feature_id": {
+ "name": "feature_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "difficultyNotes": {
+ "name": "difficultyNotes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "priceNotes": {
+ "name": "priceNotes",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "feature_translation_id": {
+ "name": "feature_translation_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "image": {
+ "name": "image",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "key": {
+ "name": "key",
+ "type": "varchar(1024)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "width": {
+ "name": "width",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "height": {
+ "name": "height",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "source": {
+ "name": "source",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "captureDate": {
+ "name": "captureDate",
+ "type": "date",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "alt": {
+ "name": "alt",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "blurDataURL": {
+ "name": "blurDataURL",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "image_id": {
+ "name": "image_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "placeListToPlace": {
+ "name": "placeListToPlace",
+ "columns": {
+ "placeListId": {
+ "name": "placeListId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "placeId": {
+ "name": "placeId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "addedAt": {
+ "name": "addedAt",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "placeListToPlace_placeId_placeListId_pk": {
+ "name": "placeListToPlace_placeId_placeListId_pk",
+ "columns": [
+ "placeId",
+ "placeListId"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "placeList": {
+ "name": "placeList",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "placeList_id": {
+ "name": "placeList_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "placeCategory": {
+ "name": "placeCategory",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "icon": {
+ "name": "icon",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "color": {
+ "name": "color",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "hasVisitMission": {
+ "name": "hasVisitMission",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "name": {
+ "name": "name",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "namePlural": {
+ "name": "namePlural",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "nameGender": {
+ "name": "nameGender",
+ "type": "enum('masculine','feminine')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "placeCategory_id": {
+ "name": "placeCategory_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "placeCategory_translation": {
+ "name": "placeCategory_translation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "placeCategory_id": {
+ "name": "placeCategory_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "namePlural": {
+ "name": "namePlural",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "nameGender": {
+ "name": "nameGender",
+ "type": "enum('masculine','feminine')",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "placeCategory_translation_id": {
+ "name": "placeCategory_translation_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "place": {
+ "name": "place",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "mainImageId": {
+ "name": "mainImageId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "location": {
+ "name": "location",
+ "type": "POINT SRID 25831",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "mainCategoryId": {
+ "name": "mainCategoryId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "featuresId": {
+ "name": "featuresId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "verificationRequirementsId": {
+ "name": "verificationRequirementsId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "place_id": {
+ "name": "place_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "placeToPlaceCategory": {
+ "name": "placeToPlaceCategory",
+ "columns": {
+ "placeId": {
+ "name": "placeId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "categoryId": {
+ "name": "categoryId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "placeToPlaceCategory_categoryId_placeId_pk": {
+ "name": "placeToPlaceCategory_categoryId_placeId_pk",
+ "columns": [
+ "categoryId",
+ "placeId"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "place_translation": {
+ "name": "place_translation",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "place_id": {
+ "name": "place_id",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "locale": {
+ "name": "locale",
+ "type": "varchar(10)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "description": {
+ "name": "description",
+ "type": "tinytext",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "content": {
+ "name": "content",
+ "type": "text",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "place_translation_id": {
+ "name": "place_translation_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "user": {
+ "name": "user",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "name": {
+ "name": "name",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "hashedPassword": {
+ "name": "hashedPassword",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "email": {
+ "name": "email",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "emailVerified": {
+ "name": "emailVerified",
+ "type": "timestamp(3)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "image": {
+ "name": "image",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "role": {
+ "name": "role",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'user'"
+ },
+ "visitedPlaceListId": {
+ "name": "visitedPlaceListId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "user_id": {
+ "name": "user_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {
+ "user_email_unique": {
+ "name": "user_email_unique",
+ "columns": [
+ "email"
+ ]
+ }
+ }
+ },
+ "verificationRequirement": {
+ "name": "verificationRequirement",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "isLocationRequired": {
+ "name": "isLocationRequired",
+ "type": "boolean",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": true
+ },
+ "maxLocationDistance": {
+ "name": "maxLocationDistance",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verificationRequirement_id": {
+ "name": "verificationRequirement_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ },
+ "verification": {
+ "name": "verification",
+ "columns": {
+ "id": {
+ "name": "id",
+ "type": "serial",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": true
+ },
+ "placeId": {
+ "name": "placeId",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "userId": {
+ "name": "userId",
+ "type": "varchar(255)",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false
+ },
+ "validatedOn": {
+ "name": "validatedOn",
+ "type": "timestamp",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "CURRENT_TIMESTAMP"
+ },
+ "deviceLocation": {
+ "name": "deviceLocation",
+ "type": "POINT SRID 25831",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ },
+ "deviceLocationAccuracy": {
+ "name": "deviceLocationAccuracy",
+ "type": "int",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false
+ }
+ },
+ "indexes": {},
+ "foreignKeys": {},
+ "compositePrimaryKeys": {
+ "verification_id": {
+ "name": "verification_id",
+ "columns": [
+ "id"
+ ]
+ }
+ },
+ "uniqueConstraints": {}
+ }
+ },
+ "schemas": {},
+ "_meta": {
+ "schemas": {},
+ "tables": {},
+ "columns": {}
+ }
+}
\ No newline at end of file
diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json
index 80bd7b04..5d5a7a01 100644
--- a/drizzle/meta/_journal.json
+++ b/drizzle/meta/_journal.json
@@ -127,6 +127,13 @@
"when": 1707913502270,
"tag": "0017_tricky_hellfire_club",
"breakpoints": true
+ },
+ {
+ "idx": 18,
+ "version": "5",
+ "when": 1708040528072,
+ "tag": "0018_blushing_daimon_hellstrom",
+ "breakpoints": true
}
]
}
\ No newline at end of file
diff --git a/e2e-tests/explore.test.ts b/e2e-tests/explore.test.ts
index 184c6c48..b629fe1a 100644
--- a/e2e-tests/explore.test.ts
+++ b/e2e-tests/explore.test.ts
@@ -9,7 +9,11 @@ test('has title', async ({ page }) => {
test('place link works', async ({ page }) => {
await page.goto('/en/explore')
- await page.getByRole('link', { name: 'Sa Tuna' }).click()
+ const link = page.getByRole('link', { name: 'Sa Riera Beach' })
+ await link.scrollIntoViewIfNeeded()
+ await link.click()
- await expect(page.getByRole('heading', { name: 'Sa Tuna' })).toBeVisible()
+ await expect(
+ page.getByRole('heading', { name: 'Sa Riera Beach' })
+ ).toBeVisible()
})
diff --git a/src/app/[locale]/(app)/explore/_components/features-block.tsx b/src/app/[locale]/(app)/explore/_components/features-block.tsx
index d0680a8d..e41ede2a 100644
--- a/src/app/[locale]/(app)/explore/_components/features-block.tsx
+++ b/src/app/[locale]/(app)/explore/_components/features-block.tsx
@@ -5,13 +5,13 @@ import { useTranslations } from 'next-intl'
import { FC, PropsWithChildren } from 'react'
import { MarkdownContent } from '~/components/generic/markdown-content'
import { IntlMessageKeys } from '~/helpers/types'
-import { pick } from '~/helpers/utilities'
import { Features } from '~/server/db/constants/features'
import {
featureDisplayGroups,
getCompositeFeatureKey,
+ getCompositeFeatureValues,
getIconForFeature,
- getMoreInfoContent,
+ useFeatureDisplay,
} from '~/server/db/constants/features-display-data'
export const FeaturesBlock: FC<{ features: Features; className?: string }> = ({
@@ -20,118 +20,124 @@ export const FeaturesBlock: FC<{ features: Features; className?: string }> = ({
}) => {
const t = useTranslations('data.features')
+ const { allValuesNull, allValuesNullInGroup, getMoreInfoContent } =
+ useFeatureDisplay(features)
+
+ if (allValuesNull) return null
+
return (
- {featureDisplayGroups.map((group) => (
-
- {group.featureDisplays.map((featureDisplay) => {
- if ('hidden' in featureDisplay && featureDisplay.hidden) {
- return null
- }
-
- switch (featureDisplay.type) {
- case 'number':
- case 'text': {
- const value = features[featureDisplay.key]
- if (value === null || value === undefined) return null
-
- if ('showRaw' in featureDisplay && featureDisplay.showRaw) {
- return (
-
- )
- } else {
- return (
- }`,
- {
- value,
- }
- )}
- moreInfo={getMoreInfoContent(featureDisplay, features)}
- />
- )
- }
- }
- case 'markdown': {
- const value = features[featureDisplay.key]
- if (value === null || value === undefined) return null
- return (
-
- )
- }
- case 'enum': {
- const value = features[featureDisplay.key]
- if (value === null || value === undefined) return null
-
- return (
- }`
- )}
- moreInfo={getMoreInfoContent(featureDisplay, features)}
- />
- )
- }
- case 'boolean': {
- const value = features[featureDisplay.key]
- if (value === null || value === undefined) return null
-
- return (
-
- )
- }
- case 'composite': {
- const rawValues = pick(
- features,
- featureDisplay.keys
- ) as Parameters<(typeof featureDisplay)['transformValues']>[0]
-
- if (
- featureDisplay.showIf &&
- !featureDisplay.showIf(rawValues)
- ) {
+ {featureDisplayGroups.map(
+ (group) =>
+ !allValuesNullInGroup[group.key] && (
+
+ {group.featureDisplays.map((featureDisplay) => {
+ if ('hidden' in featureDisplay && featureDisplay.hidden) {
return null
}
- const values = featureDisplay?.transformValues(rawValues)
-
- const key = getCompositeFeatureKey(featureDisplay.keys)
- return (
-
- )
- }
- }
- })}
-
- ))}
+ switch (featureDisplay.type) {
+ case 'number':
+ case 'text': {
+ const value = features[featureDisplay.key]
+ if (value === null || value === undefined) return null
+
+ if (
+ 'showRaw' in featureDisplay &&
+ featureDisplay.showRaw
+ ) {
+ return (
+
+ )
+ } else {
+ return (
+ }`,
+ {
+ value,
+ }
+ )}
+ moreInfo={getMoreInfoContent(featureDisplay)}
+ />
+ )
+ }
+ }
+ case 'markdown': {
+ const value = features[featureDisplay.key]
+ if (value === null || value === undefined) return null
+ return (
+
+ )
+ }
+ case 'enum': {
+ const value = features[featureDisplay.key]
+ if (value === null || value === undefined) return null
+
+ return (
+ }`
+ )}
+ moreInfo={getMoreInfoContent(featureDisplay)}
+ />
+ )
+ }
+ case 'boolean': {
+ const value = features[featureDisplay.key]
+ if (value === null || value === undefined) return null
+
+ return (
+
+ )
+ }
+ case 'composite': {
+ const values = getCompositeFeatureValues(
+ featureDisplay,
+ features
+ )
+
+ if (!values) return null
+
+ const key = getCompositeFeatureKey(featureDisplay.keys)
+ return (
+
+ )
+ }
+ }
+ })}
+
+ )
+ )}
)
diff --git a/src/app/[locale]/(app)/explore/_components/place-details.tsx b/src/app/[locale]/(app)/explore/_components/place-details.tsx
index 38958cb2..ba11fb2e 100644
--- a/src/app/[locale]/(app)/explore/_components/place-details.tsx
+++ b/src/app/[locale]/(app)/explore/_components/place-details.tsx
@@ -84,9 +84,7 @@ export const PlaceDetails: FC<{
className="mt-2"
/>
- {place.features && (
-
- )}
+
{place.content ? (
diff --git a/src/app/[locale]/admin/places/__components/place-form.tsx b/src/app/[locale]/admin/places/__components/place-form.tsx
index dacd8ceb..2c85b730 100644
--- a/src/app/[locale]/admin/places/__components/place-form.tsx
+++ b/src/app/[locale]/admin/places/__components/place-form.tsx
@@ -65,7 +65,7 @@ export const PlaceForm: FC<{
location: undefined,
mainImageId: undefined,
content: undefined,
- features: undefined,
+ features: {},
},
})
diff --git a/src/schemas/places.ts b/src/schemas/places.ts
index 0e00807f..4c93034d 100644
--- a/src/schemas/places.ts
+++ b/src/schemas/places.ts
@@ -49,7 +49,7 @@ export const createPlaceSchema = z.object({
),
mainImageId: z.number().int().optional().nullable(),
content: z.string().optional(),
- features: createInsertSchema(features).optional().nullable(),
+ features: createInsertSchema(features),
})
export const editPlaceSchema = createPlaceSchema.extend({
diff --git a/src/server/api/router/admin/places.ts b/src/server/api/router/admin/places.ts
index aba318f8..91600b9c 100644
--- a/src/server/api/router/admin/places.ts
+++ b/src/server/api/router/admin/places.ts
@@ -176,27 +176,20 @@ export const placesAdminRouter = router({
await db.transaction(async (tx) => {
const placeId = Number(input.id)
- let featuresId = (
+ const featuresId = (
await tx
.selectDistinct({ featuresId: places.featuresId })
.from(places)
.where(eq(places.id, placeId))
)[0].featuresId
- if (featuresId === null) {
- const insertFeaturesResult = await tx
- .insert(features)
- .values({ ...input.features })
- featuresId = Number(insertFeaturesResult.insertId)
- } else {
- await tx
- .update(features)
- .set({
- ...input.features,
- id: featuresId,
- })
- .where(eq(features.id, featuresId))
- }
+ await tx
+ .update(features)
+ .set({
+ id: featuresId,
+ ...input.features,
+ })
+ .where(eq(features.id, featuresId))
await tx
.update(places)
diff --git a/src/server/db/constants/features-display-data.ts b/src/server/db/constants/features-display-data.ts
index 19abfab9..040bc179 100644
--- a/src/server/db/constants/features-display-data.ts
+++ b/src/server/db/constants/features-display-data.ts
@@ -31,9 +31,12 @@ import {
IconToolsKitchen2Off,
IconWalk,
} from '@tabler/icons-react'
-import { InferInsertModel, InferSelectModel } from 'drizzle-orm'
+import { useMemo } from 'react'
import { Join } from 'ts-toolbelt/out/String/Join'
+import { pick } from '~/helpers/utilities'
import {
+ FeaturesInsert,
+ FeaturesSelect,
PriceUnit,
amountOfPeople,
difficulty,
@@ -41,7 +44,6 @@ import {
placeToArriveFrom,
priceUnit,
} from '~/server/db/constants/features'
-import { features } from '~/server/db/schema'
const typeFeatureDisplay = (feature: F) => feature
@@ -244,17 +246,6 @@ export const featureDisplayGroups = [
featureDisplays: AnyFeature[]
}[]
-export function getMoreInfoContent(
- featureDisplay: AnyFeature,
- features: Features | null | undefined
-) {
- if (!features) return null
- if (!('moreInfoFeatureKey' in featureDisplay)) return null
- if (!featureDisplay.moreInfoFeatureKey) return null
-
- return features[featureDisplay.moreInfoFeatureKey]
-}
-
export function getIconForFeature(
featureDisplay: F,
value: string | boolean | null | undefined
@@ -269,11 +260,87 @@ export function getCompositeFeatureKey>(keys: Keys) {
return keys.join('-') as Join
}
+function getCompositeFeatureRawValues(
+ featureDisplay: T,
+ features: Features
+) {
+ return pick(features, featureDisplay.keys) as Parameters<
+ NonNullable
+ >[0]
+}
+
+function shouldShow(
+ featureDisplay: T,
+ rawValues: Parameters>[0]
+) {
+ if (!featureDisplay.showIf) return true
+ return featureDisplay.showIf(rawValues)
+}
+
+export function getCompositeFeatureValues(
+ featureDisplay: T,
+ features: Features
+) {
+ const rawValues = getCompositeFeatureRawValues(featureDisplay, features)
+ if (!shouldShow(featureDisplay, rawValues)) return null
+
+ if (!featureDisplay?.transformValues) return rawValues
+ return featureDisplay?.transformValues(rawValues)
+}
+
+export function useFeatureDisplay(features: Features | null | undefined) {
+ const allValuesNullInGroup = useMemo(() => {
+ return Object.fromEntries(
+ featureDisplayGroups.map((group) => [
+ group.key,
+ group.featureDisplays.every(featureDisplayIsEmpty),
+ ])
+ ) as Record<(typeof featureDisplayGroups)[number]['key'], boolean>
+ }, [features])
+
+ const allValuesNull = useMemo(() => {
+ return featureDisplayGroups.every(
+ (group) => allValuesNullInGroup[group.key]
+ )
+ }, [allValuesNullInGroup])
+
+ function getMoreInfoContent(featureDisplay: AnyFeature) {
+ if (!features) return null
+ if (!('moreInfoFeatureKey' in featureDisplay)) return null
+ if (!featureDisplay.moreInfoFeatureKey) return null
+
+ return features[featureDisplay.moreInfoFeatureKey]
+ }
+
+ function featureDisplayIsEmpty(featureDisplay: AnyFeature) {
+ if (!features) return true
+
+ if ('hidden' in featureDisplay && featureDisplay.hidden) {
+ return true
+ }
+
+ if (featureDisplay.type === 'composite') {
+ const rawValues = getCompositeFeatureRawValues(featureDisplay, features)
+ return !shouldShow(featureDisplay, rawValues)
+ }
+
+ return (
+ features[featureDisplay.key] === null ||
+ features[featureDisplay.key] === undefined
+ )
+ }
+
+ return {
+ allValuesNull,
+ allValuesNullInGroup,
+ getMoreInfoContent,
+ featureDisplayIsEmpty,
+ }
+}
+
// ------------------- types -------------------
-type Features =
- | InferSelectModel
- | InferInsertModel
+type Features = FeaturesInsert | FeaturesSelect
type FeatureKey = Exclude
diff --git a/src/server/db/constants/features.ts b/src/server/db/constants/features.ts
index 776a1d2e..340e176c 100644
--- a/src/server/db/constants/features.ts
+++ b/src/server/db/constants/features.ts
@@ -1,4 +1,4 @@
-import { InferSelectModel } from 'drizzle-orm'
+import { InferInsertModel, InferSelectModel } from 'drizzle-orm'
import { features } from '../schema'
// Don't reorder these values, they are used to generate the database enum.
@@ -45,4 +45,6 @@ export const placeToArriveFrom = [
] as const
export type PlaceToArriveFrom = (typeof placeToArriveFrom)[number]
-export type Features = InferSelectModel
+export type FeaturesSelect = InferSelectModel
+export type FeaturesInsert = InferInsertModel
+export type Features = FeaturesSelect
diff --git a/src/server/db/schema/places.ts b/src/server/db/schema/places.ts
index 02fbaf4b..3dca8429 100644
--- a/src/server/db/schema/places.ts
+++ b/src/server/db/schema/places.ts
@@ -31,7 +31,7 @@ export const {
mainImageId: int('mainImageId'),
location: pointType('location').notNull(),
mainCategoryId: int('mainCategoryId').notNull(),
- featuresId: int('featuresId'),
+ featuresId: int('featuresId').notNull(),
verificationRequirementsId: int('verificationRequirementsId'),
},
translatableColumns: {