feat(SW-66, SW-348): search functionality and ui

This commit is contained in:
Simon Emanuelsson
2024-08-28 10:47:57 +02:00
parent b9dbcf7d90
commit af850c90e7
437 changed files with 7663 additions and 9881 deletions
+1 -1
View File
@@ -18,7 +18,7 @@ export const userMutationRouter = router({
"api.user.creditCard.add start",
JSON.stringify({ query: { language: input.language } })
)
const apiResponse = await api.post(api.endpoints.v1.intiateSaveCard, {
const apiResponse = await api.post(api.endpoints.v1.initiateSaveCard, {
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
+45 -31
View File
@@ -1,40 +1,54 @@
import { z } from "zod"
import { countriesMap } from "@/components/TempDesignSystem/Form/Country/countries"
import { getMembership } from "@/utils/user"
export const getUserSchema = z.object({
address: z.object({
city: z.string().optional(),
country: z.string().optional(),
countryCode: z.nativeEnum(countriesMap).optional(),
streetAddress: z.string().optional(),
zipCode: z.string().optional(),
}),
dateOfBirth: z.string().optional().default("1900-01-01"),
email: z.string().email(),
firstName: z.string(),
language: z.string().optional(),
lastName: z.string(),
memberships: z.array(
z.object({
currentPoints: z.number(),
expirationDate: z.string(),
membershipNumber: z.string(),
membershipLevel: z.string().optional(),
memberSince: z.string(),
membershipType: z.string(),
nextLevel: z.string().optional(),
nightsToTopTier: z.number().optional(),
pointsExpiryDate: z.string().optional(),
pointsRequiredToNextlevel: z.number().optional(),
pointsToExpire: z.number().optional(),
tierExpirationDate: z.string().optional(),
})
),
phoneNumber: z.string().optional(),
profileId: z.string(),
export const membershipSchema = z.object({
currentPoints: z.number(),
expirationDate: z.string(),
membershipNumber: z.string(),
membershipLevel: z.string().optional(),
memberSince: z.string(),
membershipType: z.string(),
nextLevel: z.string().optional(),
nightsToTopTier: z.number().optional(),
pointsExpiryDate: z.string().optional(),
pointsRequiredToNextlevel: z.number().optional(),
pointsToExpire: z.number().optional(),
tierExpirationDate: z.string().optional(),
})
export const getUserSchema = z
.object({
data: z.object({
attributes: z.object({
address: z.object({
city: z.string().optional(),
country: z.string().optional(),
countryCode: z.nativeEnum(countriesMap).optional(),
streetAddress: z.string().optional(),
zipCode: z.string().optional(),
}),
dateOfBirth: z.string().optional().default("1900-01-01"),
email: z.string().email(),
firstName: z.string(),
language: z.string().optional(),
lastName: z.string(),
memberships: z.array(membershipSchema),
phoneNumber: z.string().optional(),
profileId: z.string(),
}),
type: z.string(),
}),
})
.transform((apiResponse) => {
return {
...apiResponse.data.attributes,
membership: getMembership(apiResponse.data.attributes.memberships),
name: `${apiResponse.data.attributes.firstName} ${apiResponse.data.attributes.lastName}`,
}
})
// Schema is the same for upcoming and previous stays endpoints
export const getStaysSchema = z.object({
data: z.array(
+6 -125
View File
@@ -1,7 +1,5 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import {
protectedProcedure,
@@ -13,26 +11,24 @@ import { countries } from "@/components/TempDesignSystem/Form/Country/countries"
import * as maskValue from "@/utils/maskValue"
import { getMembership, getMembershipCards } from "@/utils/user"
import encryptValue from "../utils/encryptValue"
import { friendTransactionsInput, staysInput } from "./input"
import {
creditCardsSchema,
FriendTransaction,
getFriendTransactionsSchema,
getMembershipCardsSchema,
getStaysSchema,
getUserSchema,
Stay,
} from "./output"
import { benefits, extendedUser, nextLevelPerks } from "./temp"
import { updateStaysBookingUrl } from "./utils"
import type { Session } from "next-auth"
import { RewardTransactionTypes } from "@/types/components/myPages/myPage/enums"
import type {
LoginType,
TrackingSDKUserData,
} from "@/types/components/tracking"
import { BlocksEnums } from "@/types/enums/blocks"
import { Transactions } from "@/types/enums/transactions"
import type { MembershipLevel } from "@/constants/membershipLevels"
// OpenTelemetry metrics: User
@@ -93,7 +89,6 @@ export async function getVerifiedUser({ session }: { session: Session }) {
getVerifiedUserCounter.add(1)
console.info("api.user.profile getVerifiedUser start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${session.token.access_token}`,
},
@@ -147,7 +142,7 @@ export async function getVerifiedUser({ session }: { session: Session }) {
return null
}
const verifiedData = getUserSchema.safeParse(apiJson.data.attributes)
const verifiedData = getUserSchema.safeParse(apiJson)
if (!verifiedData.success) {
getVerifiedUserFailCounter.add(1, {
error_type: "validation_error",
@@ -166,109 +161,6 @@ export async function getVerifiedUser({ session }: { session: Session }) {
return verifiedData
}
function fakingRequest<T>(payload: T): Promise<T> {
return new Promise((resolve) => {
setTimeout(() => {
resolve(payload)
}, 1500)
})
}
async function updateStaysBookingUrl(
data: Stay[],
token: string,
lang: Lang
): Promise<Stay[]>
async function updateStaysBookingUrl(
data: FriendTransaction[],
token: string,
lang: Lang
): Promise<FriendTransaction[]>
async function updateStaysBookingUrl(
data: Stay[] | FriendTransaction[],
token: string,
lang: Lang
) {
// Tenporary API call needed till we have user name in ctx session data
getProfileCounter.add(1)
console.info("api.user.profile updatebookingurl start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${token}`,
},
})
// Temporary Url, domain and lang support for current web
let localeDomain = env.PUBLIC_URL
let fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
switch (lang) {
case Lang.sv:
localeDomain = localeDomain?.replace(".com", ".se")
fullBookingUrl = localeDomain + "/hotelreservation/din-bokning"
break
case Lang.no:
localeDomain = localeDomain?.replace(".com", ".no")
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
break
case Lang.da:
localeDomain = localeDomain?.replace(".com", ".dk")
fullBookingUrl = localeDomain + "/hotelreservation/min-booking"
break
case Lang.fi:
localeDomain = localeDomain?.replace(".com", ".fi")
fullBookingUrl = localeDomain + "/varaa-hotelli/varauksesi"
break
case Lang.de:
localeDomain = localeDomain?.replace(".com", ".de")
fullBookingUrl = localeDomain + "/hotelreservation/my-booking"
break
default:
break
}
if (apiResponse.ok) {
getProfileSuccessCounter.add(1)
console.info(
"api.user.profile updatebookingurl success",
JSON.stringify({})
)
const apiJson = await apiResponse.json()
if (apiJson.data?.attributes) {
return data.map((d) => {
const originalString =
d.attributes.confirmationNumber.toString() +
"," +
apiJson.data.attributes.lastName
const encryptedBookingValue = encryptValue(originalString)
const bookingUrl = !!encryptedBookingValue
? fullBookingUrl + "?RefId=" + encryptedBookingValue
: fullBookingUrl +
"?lastName=" +
apiJson.data.attributes.lastName +
"&bookingId=" +
d.attributes.confirmationNumber
return {
...d,
attributes: {
...d.attributes,
bookingUrl: bookingUrl,
},
}
})
}
}
getProfileFailCounter.add(1, { error: JSON.stringify(apiResponse) })
console.info(
"api.user.profile updatebookingurl error",
JSON.stringify({ error: apiResponse })
)
return data
}
export const userQueryRouter = router({
get: protectedProcedure
.use(async function (opts) {
@@ -299,7 +191,6 @@ export const userQueryRouter = router({
)
const user = {
...extendedUser,
address: {
city: verifiedData.data.address.city,
country: country?.name ?? "",
@@ -312,6 +203,7 @@ export const userQueryRouter = router({
firstName: verifiedData.data.firstName,
language: verifiedData.data.language,
lastName: verifiedData.data.lastName,
membership: getMembership(verifiedData.data.memberships),
memberships: verifiedData.data.memberships,
name: `${verifiedData.data.firstName} ${verifiedData.data.lastName}`,
phoneNumber: verifiedData.data.phoneNumber,
@@ -460,16 +352,6 @@ export const userQueryRouter = router({
}
return loggedInUserTrackingData
}),
benefits: router({
current: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof benefits>(benefits)
}),
nextLevel: protectedProcedure.query(async function (opts) {
// TODO: Make request to get user data from Scandic API
return await fakingRequest<typeof nextLevelPerks>(nextLevelPerks)
}),
}),
stays: router({
previous: protectedProcedure
@@ -735,7 +617,7 @@ export const userQueryRouter = router({
)
const pageData = updatedData
.filter((t) => t.type !== RewardTransactionTypes.expired)
.filter((t) => t.type !== Transactions.rewardType.expired)
.sort((a, b) => {
// 'BALFWD' are transactions from Opera migration that happended in May 2021
if (a.attributes.confirmationNumber === "BALFWD") return 1
@@ -787,7 +669,6 @@ export const userQueryRouter = router({
getCreditCardsCounter.add(1)
console.info("api.profile.creditCards start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.creditCards, {
cache: "no-store",
headers: {
Authorization: `Bearer ${ctx.session.token.access_token}`,
},
-123
View File
@@ -1,123 +0,0 @@
export const benefits = [
{
id: 1,
value: "€5 voucher",
explanation: "to spend in bar & restaurant for each night",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
{
id: 2,
value: "Breakfast to go",
explanation: "for early birds, when staying",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
{
id: 3,
value: "15% discount",
explanation: "in the restaurant & the bar",
subtitle:
"Lorem ipsum dolor sit amet consectetur. Pharetra lectus sagittis turpis blandit feugiat amet enim massa.",
href: "#",
},
]
export const challenges = {
journeys: [
{
tag: "After work queen",
title: "Try 3 Hotel Bars, Pocket 200 Points",
},
{
tag: "Dine & Shine",
title: "Visit 3 scandic Restaurants, Earn 150 Points",
},
],
victories: [
{
tag: "Capital Explorer",
title: "Stay in 3 scandic hotels, in three Capitals, Gain 2000 Points",
},
{
tag: "Friends Feast",
title: "Dine with 3 Buddies, Snag a Free Breakfast",
},
{
tag: "Eco Warrior",
title: "Choose Green, Get 500 Points",
},
],
}
export const nextLevelPerks = {
nextLevel: "Close Friend",
perks: [
{
id: 1,
explanation: "Free soft drink voucher for the kids when staying",
},
{
id: 2,
explanation: "Free early check in",
},
{
id: 3,
explanation: "25% extra bonus points on each stay",
},
{
id: 4,
explanation: "25% extra bonus points on each stay",
},
],
}
export const shortcuts = [
{
href: "#",
title: "My Benefit",
},
{
href: "#",
title: "Program overview",
},
// {
// href: "#",
// title: "Scandic Friends shop",
// },
// {
// href: "#",
// title: "Fire and safety",
// },
// {
// href: "#",
// title: "Our sustainability work",
// },
// {
// href: "#",
// title: "How you earn points",
// },
// {
// href: "#",
// title: "How you use points",
// },
// {
// href: "#",
// title: "Missing points",
// },
// {
// href: "#",
// title: "Our term and conditions",
// },
]
export const extendedUser = {
journeys: challenges.journeys,
nights: 14,
shortcuts,
victories: challenges.victories,
}
+116
View File
@@ -0,0 +1,116 @@
import { metrics } from "@opentelemetry/api"
import { Lang } from "@/constants/languages"
import { env } from "@/env/server"
import * as api from "@/lib/api"
import encryptValue from "../utils/encryptValue"
import type { FriendTransaction, Stay } from "./output"
const meter = metrics.getMeter("trpc.user")
const getProfileCounter = meter.createCounter("trpc.user.profile")
const getProfileSuccessCounter = meter.createCounter("trpc.user.profile-success")
const getProfileFailCounter = meter.createCounter("trpc.user.profile-fail")
async function updateStaysBookingUrl(
data: Stay[],
token: string,
lang: Lang
): Promise<Stay[]>
async function updateStaysBookingUrl(
data: FriendTransaction[],
token: string,
lang: Lang
): Promise<FriendTransaction[]>
async function updateStaysBookingUrl(
data: Stay[] | FriendTransaction[],
token: string,
lang: Lang
) {
// Temporary API call needed till we have user name in ctx session data
getProfileCounter.add(1)
console.info("api.user.profile updatebookingurl start", JSON.stringify({}))
const apiResponse = await api.get(api.endpoints.v1.profile, {
cache: "no-store",
headers: {
Authorization: `Bearer ${token}`,
},
})
// Temporary Url, domain and lang support for current web
const bookingUrl = new URL(
"/hotelreservation/my-booking",
env.PUBLIC_URL ?? ""
)
switch (lang) {
case Lang.sv:
bookingUrl.host = bookingUrl.host.replace(".com", ".se")
bookingUrl.pathname = "/hotelreservation/din-bokning"
break
case Lang.no:
bookingUrl.host = bookingUrl.host.replace(".com", ".no")
bookingUrl.pathname = "/hotelreservation/my-booking"
break
case Lang.da:
bookingUrl.host = bookingUrl.host.replace(".com", ".dk")
bookingUrl.pathname = "/hotelreservation/min-booking"
break
case Lang.fi:
bookingUrl.host = bookingUrl.host.replace(".com", ".fi")
bookingUrl.pathname = "/varaa-hotelli/varauksesi"
break
case Lang.de:
bookingUrl.host = bookingUrl.host.replace(".com", ".de")
bookingUrl.pathname = "/hotelreservation/my-booking"
break
default:
break
}
if (!apiResponse.ok) {
getProfileFailCounter.add(1, { error: JSON.stringify(apiResponse) })
console.info(
"api.user.profile updatebookingurl error",
JSON.stringify({ error: apiResponse })
)
return data
}
const apiJson = await apiResponse.json()
if (!apiJson.data?.attributes) {
return data
}
getProfileSuccessCounter.add(1)
console.info(
"api.user.profile updatebookingurl success",
JSON.stringify({})
)
return data.map((d) => {
const originalString =
d.attributes.confirmationNumber.toString() +
"," +
apiJson.data.attributes.lastName
const encryptedBookingValue = encryptValue(originalString)
if (!!encryptedBookingValue) {
bookingUrl.searchParams.set("RefId", encryptedBookingValue)
} else {
bookingUrl.searchParams.set("lastName", apiJson.data.attributes.lastName)
bookingUrl.searchParams.set(
"bookingId",
d.attributes.confirmationNumber
)
}
return {
...d,
attributes: {
...d.attributes,
bookingUrl: bookingUrl.toString(),
},
}
})
}
export { updateStaysBookingUrl }