Merged in SW-3490-set-metadata-for-routes (pull request #2881)

SW-3490 set metadata for routes
* feat(SW-3490): Set metadata title for hotelreservation paths

Approved-by: Anton Gunnarsson
This commit is contained in:
Joakim Jäderberg
2025-10-01 11:34:52 +00:00
parent 4f151b143e
commit df8e223d23
25 changed files with 440 additions and 37 deletions

View File

@@ -1,9 +1,50 @@
import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage" import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage"
import { getHotel } from "@/lib/trpc/memoizedRequests/getHotel"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsMapPage( export default async function AlternativeHotelsMapPage(
props: PageArgs<LangParams> props: PageArgs<LangParams>
) { ) {

View File

@@ -1,9 +1,50 @@
import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage" import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import { getHotel } from "@/lib/trpc/memoizedRequests/getHotel"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import { type LangParams, type PageArgs } from "@/types/params" import { type LangParams, type PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsPage( export default async function AlternativeHotelsPage(
props: PageArgs<LangParams> props: PageArgs<LangParams>
) { ) {

View File

@@ -1,9 +1,26 @@
import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage" import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams>): Promise<Metadata> {
const { city } = await searchParams
if (!city || Array.isArray(city)) {
return {}
}
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelMapPage(props: PageArgs<LangParams>) { export default async function SelectHotelMapPage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const lang = await getLang() const lang = await getLang()

View File

@@ -1,9 +1,26 @@
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage" import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams>): Promise<Metadata> {
const { city } = await searchParams
if (!city || Array.isArray(city)) {
return {}
}
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelPage(props: PageArgs<LangParams>) { export default async function SelectHotelPage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const lang = await getLang() const lang = await getLang()

View File

@@ -1,9 +1,39 @@
import { SelectRatePage as SelectRatePagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectRatePage" import { SelectRatePage as SelectRatePagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectRatePage"
import { getHotel } from "@/lib/trpc/memoizedRequests/getHotel"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import { type LangParams, type PageArgs } from "@/types/params" import { type LangParams, type PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams>): Promise<Metadata> {
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
if (!hotelData?.additionalData?.name) {
return {}
}
return {
title: hotelData.additionalData.name,
}
}
export default async function SelectRatePage(props: PageArgs<LangParams>) { export default async function SelectRatePage(props: PageArgs<LangParams>) {
const searchParams = await props.searchParams const searchParams = await props.searchParams
const lang = await getLang() const lang = await getLang()

View File

@@ -38,7 +38,6 @@ import type { LangRoute } from "@scandic-hotels/common/constants/routes/langRout
import type { Metadata } from "next" import type { Metadata } from "next"
export const metadata: Metadata = { export const metadata: Metadata = {
title: "SAS by Scandic Hotels",
description: "TODO This text should be updated.", description: "TODO This text should be updated.",
} }

View File

@@ -2,8 +2,25 @@ import "@scandic-hotels/common/polyfills"
import { configureTrpc } from "@/lib/trpc" import { configureTrpc } from "@/lib/trpc"
import { getTitlePrefix } from "@/util/metadata/getTitlePrfiex"
import type { Metadata } from "next"
configureTrpc() configureTrpc()
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
template: combineSegments([
getTitlePrefix(),
"%s",
"SAS by Scandic Hotels",
]),
default: combineSegments([getTitlePrefix(), "SAS by Scandic Hotels"]),
},
}
}
export default function RootLayout({ export default function RootLayout({
children, children,
}: { }: {
@@ -11,3 +28,10 @@ export default function RootLayout({
}) { }) {
return <>{children}</> return <>{children}</>
} }
function combineSegments(
segments: (string | null | undefined)[],
delimiter = " | "
) {
return segments.filter(Boolean).join(delimiter).trim()
}

View File

@@ -0,0 +1,14 @@
import { cache } from "react"
import { serverClient } from ".."
import type { HotelInput } from "@scandic-hotels/trpc/types/hotel"
export const getHotel = cache(async function getMemoizedHotelData(
input: HotelInput
) {
input.isCardOnlyPayment ??= false
const caller = await serverClient()
return caller.hotel.get(input)
})

View File

@@ -0,0 +1,11 @@
import { env } from "@/env/server"
export function getTitlePrefix() {
if (env.SENTRY_ENVIRONMENT === "production") {
return ""
}
const environmentShortName =
env.SENTRY_ENVIRONMENT === "development" ? "local" : env.SENTRY_ENVIRONMENT
return `${environmentShortName}`
}

View File

@@ -1,11 +1,52 @@
import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage" import { AlternativeHotelsMapPage as AlternativeHotelsMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsMapPage"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { Metadata } from "next"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams, { hotel: string }>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsMapPage( export default async function AlternativeHotelsMapPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {

View File

@@ -1,13 +1,54 @@
import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage" import { AlternativeHotelsPage as AlternativeHotelsPagePrimitive } from "@scandic-hotels/booking-flow/pages/AlternativeHotelsPage"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getIntl } from "@/i18n"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import { import {
type LangParams, type LangParams,
type NextSearchParams, type NextSearchParams,
type PageArgs, type PageArgs,
} from "@/types/params" } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams, { hotel: string }>): Promise<Metadata> {
const intl = await getIntl()
const { hotel } = await searchParams
const { lang } = await params
if (!hotel || Array.isArray(hotel)) {
return {}
}
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
const hotelName = hotelData?.additionalData?.name
if (!hotelName) {
return {}
}
const title = intl.formatMessage(
{
defaultMessage: "Alternatives for {value}",
},
{
value: hotelName,
}
)
return { title }
}
export default async function AlternativeHotelsPage( export default async function AlternativeHotelsPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {

View File

@@ -2,6 +2,8 @@ import { EnterDetailsPage as EnterDetailsPagePrimitive } from "@scandic-hotels/b
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/metadata/generateMetadata"
export default async function DetailsPage( export default async function DetailsPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {

View File

@@ -8,6 +8,8 @@ import styles from "./page.module.css"
import type { LangParams, PageArgs } from "@/types/params" import type { LangParams, PageArgs } from "@/types/params"
export { generateMetadata } from "@/utils/metadata/generateMetadata"
export default async function HotelReservationPage( export default async function HotelReservationPage(
props: PageArgs<LangParams> props: PageArgs<LangParams>
) { ) {

View File

@@ -1,11 +1,24 @@
import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage" import { SelectHotelMapPage as SelectHotelMapPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelMapPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import styles from "./page.module.css" import styles from "./page.module.css"
import type { Metadata } from "next"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams, { city: string }>): Promise<Metadata> {
const { city } = await searchParams
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelMapPage( export default async function SelectHotelMapPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {

View File

@@ -1,9 +1,22 @@
import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage" import { SelectHotelPage as SelectHotelPagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectHotelPage"
import { toCapitalCase } from "@scandic-hotels/common/utils/toCapitalCase"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import type { LangParams, NextSearchParams, PageArgs } from "@/types/params" import type { LangParams, NextSearchParams, PageArgs } from "@/types/params"
export async function generateMetadata({
searchParams,
}: PageArgs<LangParams, { city: string }>): Promise<Metadata> {
const { city } = await searchParams
return {
title: `${toCapitalCase(city)}`,
}
}
export default async function SelectHotelPage( export default async function SelectHotelPage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {

View File

@@ -1,13 +1,39 @@
import { SelectRatePage as SelectRatePagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectRatePage" import { SelectRatePage as SelectRatePagePrimitive } from "@scandic-hotels/booking-flow/pages/SelectRatePage"
import { getHotel } from "@/lib/trpc/memoizedRequests"
import { getLang } from "@/i18n/serverContext" import { getLang } from "@/i18n/serverContext"
import type { Metadata } from "next"
import { import {
type LangParams, type LangParams,
type NextSearchParams, type NextSearchParams,
type PageArgs, type PageArgs,
} from "@/types/params" } from "@/types/params"
export async function generateMetadata({
searchParams,
params,
}: PageArgs<LangParams, { hotel: string }>): Promise<Metadata> {
const { hotel } = await searchParams
const { lang } = await params
const hotelData = await getHotel({
hotelId: hotel,
language: lang,
isCardOnlyPayment: false,
})
if (!hotelData?.additionalData?.name) {
return {}
}
return {
title: hotelData.additionalData.name,
}
}
export default async function SelectRatePage( export default async function SelectRatePage(
props: PageArgs<LangParams, NextSearchParams> props: PageArgs<LangParams, NextSearchParams>
) { ) {

View File

@@ -0,0 +1,27 @@
import { getTitlePrefix } from "@/utils/metadata/title/getTitlePrefix"
import type { Metadata } from "next"
export async function generateMetadata(): Promise<Metadata> {
return {
title: {
template: combineSegments([getTitlePrefix(), "%s", "Scandic Hotels"]),
default: combineSegments([getTitlePrefix(), "Scandic Hotels"]),
},
}
}
export default async function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}
function combineSegments(
segments: (string | null | undefined)[],
delimiter = " | "
) {
return segments.filter(Boolean).join(delimiter).trim()
}

View File

@@ -27,6 +27,7 @@ export async function generateMetadata({
const { subpage, filterFromUrl, ...otherSearchParams } = await searchParams const { subpage, filterFromUrl, ...otherSearchParams } = await searchParams
// If there are other (real) search params, we don't want to index the page as this will // If there are other (real) search params, we don't want to index the page as this will
// cause duplicate content issues. // cause duplicate content issues.
const noIndexOnSearchParams = !!Object.keys(otherSearchParams).length const noIndexOnSearchParams = !!Object.keys(otherSearchParams).length
const caller = await serverClient() const caller = await serverClient()
const { rawMetadata, alternates, robots } = const { rawMetadata, alternates, robots } =
@@ -129,7 +130,7 @@ async function getTransformedMetadata(
) { ) {
const metadata: Metadata = { const metadata: Metadata = {
metadataBase: env.PUBLIC_URL ? new URL(env.PUBLIC_URL) : undefined, metadataBase: env.PUBLIC_URL ? new URL(env.PUBLIC_URL) : undefined,
title: await getTitle(data), title: { absolute: await getTitle(data) },
description: await getDescription(data), description: await getDescription(data),
openGraph: { openGraph: {
images: getImage(data), images: getImage(data),

View File

@@ -4,8 +4,7 @@ import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstac
export async function getDestinationPageTitle( export async function getDestinationPageTitle(
data: RawMetadataSchema, data: RawMetadataSchema,
pageType: "city" | "country", pageType: "city" | "country"
suffix: string
) { ) {
const intl = await getIntl() const intl = await getIntl()
const { destinationData } = data const { destinationData } = data
@@ -33,13 +32,10 @@ export async function getDestinationPageTitle(
{ location } { location }
) )
return `${destinationTitle}${suffix}` return destinationTitle
} }
export function getDestinationFilterSeoMetaTitle( export function getDestinationFilterSeoMetaTitle(data: RawMetadataSchema) {
data: RawMetadataSchema,
suffix: string
) {
const filter = data.destinationData?.filter const filter = data.destinationData?.filter
if (!filter) { if (!filter) {
@@ -51,10 +47,10 @@ export function getDestinationFilterSeoMetaTitle(
if (foundSeoFilter) { if (foundSeoFilter) {
if (foundSeoFilter.seo_metadata?.title) { if (foundSeoFilter.seo_metadata?.title) {
return `${foundSeoFilter.seo_metadata.title}${suffix}` return foundSeoFilter.seo_metadata.title
} }
return `${foundSeoFilter.heading}${suffix}` return foundSeoFilter.heading
} }
return null return null

View File

@@ -0,0 +1,11 @@
import { env } from "@/env/server"
export function getTitlePrefix() {
if (env.SENTRY_ENVIRONMENT === "production") {
return ""
}
const environmentShortName =
env.SENTRY_ENVIRONMENT === "development" ? "local" : env.SENTRY_ENVIRONMENT
return `${environmentShortName}`
}

View File

@@ -4,6 +4,7 @@ import {
getDestinationFilterSeoMetaTitle, getDestinationFilterSeoMetaTitle,
getDestinationPageTitle, getDestinationPageTitle,
} from "./destinationPage" } from "./destinationPage"
import { getTitlePrefix } from "./getTitlePrefix"
import { getHotelPageTitle } from "./hotelPage" import { getHotelPageTitle } from "./hotelPage"
import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output" import type { RawMetadataSchema } from "@scandic-hotels/trpc/routers/contentstack/metadata/output"
@@ -17,13 +18,14 @@ function getTitleSuffix(contentType: string) {
case PageContentTypeEnum.destinationOverviewPage: case PageContentTypeEnum.destinationOverviewPage:
case PageContentTypeEnum.destinationCityPage: case PageContentTypeEnum.destinationCityPage:
case PageContentTypeEnum.destinationCountryPage: case PageContentTypeEnum.destinationCountryPage:
return " | Scandic Hotels" return "Scandic Hotels"
default: default:
return "" return ""
} }
} }
export async function getTitle(data: RawMetadataSchema) { export async function getTitle(data: RawMetadataSchema) {
const prefix = getTitlePrefix()
const suffix = getTitleSuffix(data.system.content_type_uid) const suffix = getTitleSuffix(data.system.content_type_uid)
const metadata = data.web?.seo_metadata const metadata = data.web?.seo_metadata
const isDestinationPage = [ const isDestinationPage = [
@@ -32,48 +34,41 @@ export async function getTitle(data: RawMetadataSchema) {
].includes(data.system.content_type_uid as PageContentTypeEnum) ].includes(data.system.content_type_uid as PageContentTypeEnum)
if (isDestinationPage) { if (isDestinationPage) {
const destinationFilterSeoMetaTitle = getDestinationFilterSeoMetaTitle( const destinationFilterSeoMetaTitle = getDestinationFilterSeoMetaTitle(data)
data,
suffix
)
if (destinationFilterSeoMetaTitle) { if (destinationFilterSeoMetaTitle) {
return destinationFilterSeoMetaTitle return destinationFilterSeoMetaTitle
} }
} }
if (metadata?.title) { if (metadata?.title) {
return `${metadata.title}${suffix}` return combineSegments([prefix, metadata.title, suffix])
} }
let title: string | null = null let title: string | null | undefined = null
switch (data.system.content_type_uid) { switch (data.system.content_type_uid) {
case PageContentTypeEnum.hotelPage: case PageContentTypeEnum.hotelPage:
title = await getHotelPageTitle(data) title = await getHotelPageTitle(data)
break break
case PageContentTypeEnum.destinationCityPage: case PageContentTypeEnum.destinationCityPage:
title = await getDestinationPageTitle(data, "city", suffix) title = await getDestinationPageTitle(data, "city")
break break
case PageContentTypeEnum.destinationCountryPage: case PageContentTypeEnum.destinationCountryPage:
title = await getDestinationPageTitle(data, "country", suffix) title = await getDestinationPageTitle(data, "country")
break break
default: default:
break break
} }
if (title) {
return title
}
// Fallback titles from contentstack content // Fallback titles from contentstack content
if (data.web?.breadcrumbs?.title) { title ||= data.web?.breadcrumbs?.title || data.heading || data.header?.heading
return `${data.web.breadcrumbs.title}${suffix}`
} return combineSegments([prefix, title, suffix])
if (data.heading) { }
return `${data.heading}${suffix}`
} function combineSegments(
if (data.header?.heading) { segments: (string | null | undefined)[],
return `${data.header.heading}${suffix}` delimiter = " | "
} ) {
return "" return segments.filter(Boolean).join(delimiter).trim()
} }

View File

@@ -57,11 +57,12 @@
"./utils/maskValue": "./utils/maskValue.ts", "./utils/maskValue": "./utils/maskValue.ts",
"./utils/membershipLevels": "./utils/membershipLevels.ts", "./utils/membershipLevels": "./utils/membershipLevels.ts",
"./utils/numberFormatting": "./utils/numberFormatting.ts", "./utils/numberFormatting": "./utils/numberFormatting.ts",
"./utils/rangeArray": "./utils/rangeArray.ts",
"./utils/safeTry": "./utils/safeTry.ts",
"./utils/url": "./utils/url.ts",
"./utils/phone": "./utils/phone.ts", "./utils/phone": "./utils/phone.ts",
"./utils/promiseWithTimeout": "./utils/promiseWithTimeout.ts", "./utils/promiseWithTimeout": "./utils/promiseWithTimeout.ts",
"./utils/rangeArray": "./utils/rangeArray.ts",
"./utils/safeTry": "./utils/safeTry.ts",
"./utils/toCapitalCase": "./utils/toCapitalCase.ts",
"./utils/url": "./utils/url.ts",
"./utils/zod/*": "./utils/zod/*.ts" "./utils/zod/*": "./utils/zod/*.ts"
}, },
"dependencies": { "dependencies": {

View File

@@ -0,0 +1,27 @@
import { describe, expect, it } from "vitest"
import { toCapitalCase } from "./toCapitalCase"
describe("toCapitalCase", () => {
it("should return same value for null or undefined", () => {
expect(toCapitalCase(null)).toBe(null)
expect(toCapitalCase(undefined)).toBe(undefined)
})
it("should return empty string for empty input", () => {
expect(toCapitalCase("")).toBe("")
})
it("should capitalize the first letter and lowercase the rest", () => {
expect(toCapitalCase("hello")).toBe("Hello")
expect(toCapitalCase("HELLO")).toBe("Hello")
expect(toCapitalCase("hElLo")).toBe("Hello")
expect(toCapitalCase("h")).toBe("H")
expect(toCapitalCase("H")).toBe("H")
})
it("should handle strings with spaces", () => {
expect(toCapitalCase(" hello ")).toBe("Hello")
expect(toCapitalCase(" ")).toBe("")
})
})

View File

@@ -0,0 +1,13 @@
export function toCapitalCase(value: string): string
export function toCapitalCase(value: null): null
export function toCapitalCase(value: undefined): undefined
export function toCapitalCase(
value: string | null | undefined
): string | null | undefined {
if (!value) return value
value = value.trim()
if (!value) return value
return value[0].toUpperCase() + value.slice(1).toLowerCase()
}