Merged in feat/sw-3225-move-parking-information-to-booking-flow (pull request #2614)
feat(SW-3225): Move ParkingInformation to design-system * Inline ParkingInformation types to remove trpc dependency * Move ParkingInformation to design-system * Move numberFormatting to common package * Add deps to external * Fix imports and i18n script * Add common as dependency * Merge branch 'master' into feat/sw-3225-move-parking-information-to-booking-flow Approved-by: Linus Flood
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
'use client'
|
||||
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { Typography } from '../../Typography'
|
||||
|
||||
import styles from './parkingList.module.css'
|
||||
|
||||
import type { Parking } from '../parkingInformationTypes'
|
||||
|
||||
type ParkingListProps = Pick<
|
||||
Parking,
|
||||
| 'address'
|
||||
| 'canMakeReservation'
|
||||
| 'distanceToHotel'
|
||||
| 'numberOfChargingSpaces'
|
||||
| 'numberOfParkingSpots'
|
||||
>
|
||||
|
||||
export default function ParkingList({
|
||||
numberOfChargingSpaces,
|
||||
canMakeReservation,
|
||||
numberOfParkingSpots,
|
||||
distanceToHotel,
|
||||
address,
|
||||
}: ParkingListProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
const canMakeReservationYesMsg = intl.formatMessage({
|
||||
defaultMessage: 'Parking can be reserved in advance: Yes',
|
||||
})
|
||||
const canMakeReservationNoMsg = intl.formatMessage({
|
||||
defaultMessage: 'Parking can be reserved in advance: No',
|
||||
})
|
||||
|
||||
return (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<ul className={styles.listStyling}>
|
||||
{numberOfChargingSpaces ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage:
|
||||
'Number of charging points for electric cars: {number}',
|
||||
},
|
||||
{ number: numberOfChargingSpaces }
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
<li>
|
||||
{canMakeReservation
|
||||
? canMakeReservationYesMsg
|
||||
: canMakeReservationNoMsg}
|
||||
</li>
|
||||
{numberOfParkingSpots ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: 'Number of parking spots: {number}',
|
||||
},
|
||||
{ number: numberOfParkingSpots }
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
{distanceToHotel ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: 'Distance to hotel: {distanceInM} m',
|
||||
},
|
||||
{ distanceInM: distanceToHotel }
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
{address ? (
|
||||
<li>
|
||||
{intl.formatMessage(
|
||||
{
|
||||
defaultMessage: 'Address: {address}',
|
||||
},
|
||||
{ address }
|
||||
)}
|
||||
</li>
|
||||
) : null}
|
||||
</ul>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
.listStyling {
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.listStyling > li::before {
|
||||
content: url('/_static/icons/heart.svg');
|
||||
position: relative;
|
||||
height: 8px;
|
||||
top: 3px;
|
||||
margin-right: var(--Spacing-x1);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client'
|
||||
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { Typography } from '../../Typography'
|
||||
|
||||
import { formatPrice } from '@scandic-hotels/common/utils/numberFormatting'
|
||||
|
||||
import { type Parking, ParkingPricePeriods } from '../parkingInformationTypes'
|
||||
import { getPeriod } from './utils'
|
||||
|
||||
import styles from './parkingPrices.module.css'
|
||||
|
||||
interface ParkingPricesProps
|
||||
extends Pick<Parking['pricing'], 'freeParking'>,
|
||||
Pick<NonNullable<Parking['pricing']['localCurrency']>, 'currency'> {
|
||||
pricing: NonNullable<Parking['pricing']['localCurrency']>['ordinary']
|
||||
}
|
||||
|
||||
export default function ParkingPrices({
|
||||
currency = '',
|
||||
freeParking,
|
||||
pricing,
|
||||
}: ParkingPricesProps) {
|
||||
const intl = useIntl()
|
||||
|
||||
if (freeParking) {
|
||||
return (
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<p className={styles.wrapper}>
|
||||
{intl.formatMessage({ defaultMessage: 'Free parking' })}
|
||||
</p>
|
||||
</Typography>
|
||||
)
|
||||
}
|
||||
|
||||
const filteredPricing = pricing.filter((price) => price.amount > 0)
|
||||
|
||||
if (filteredPricing.length === 0) {
|
||||
return (
|
||||
<dl className={styles.wrapper}>
|
||||
<div className={styles.period}>
|
||||
<div className={styles.information}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<dt>{getPeriod(intl, 'Hour')}</dt>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<dd>
|
||||
{intl.formatMessage({
|
||||
defaultMessage: 'At a cost',
|
||||
})}
|
||||
</dd>
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<dl className={styles.wrapper}>
|
||||
{filteredPricing.map(({ period, amount, startTime, endTime }) => (
|
||||
<div key={period} className={styles.period}>
|
||||
<div className={styles.information}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<dt>{getPeriod(intl, period)}</dt>
|
||||
</Typography>
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<dd>{formatPrice(intl, amount, currency)}</dd>
|
||||
</Typography>
|
||||
</div>
|
||||
{startTime && endTime && period !== ParkingPricePeriods.allDay ? (
|
||||
<Typography variant="Body/Paragraph/mdRegular">
|
||||
<div className={styles.information}>
|
||||
<dt>{intl.formatMessage({ defaultMessage: 'From' })}</dt>
|
||||
<dd>{`${startTime}-${endTime}`}</dd>
|
||||
</div>
|
||||
</Typography>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</dl>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
.wrapper {
|
||||
display: grid;
|
||||
row-gap: var(--Spacing-x1);
|
||||
margin: 0;
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.period {
|
||||
display: flex;
|
||||
gap: var(--Spacing-x5);
|
||||
}
|
||||
|
||||
.information {
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.priceHeading {
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { ParkingPricePeriods } from '../parkingInformationTypes'
|
||||
|
||||
import type { IntlShape } from 'react-intl'
|
||||
|
||||
export function getPeriod(intl: IntlShape, period?: string) {
|
||||
switch (period) {
|
||||
case ParkingPricePeriods.hour:
|
||||
return intl.formatMessage({
|
||||
defaultMessage: 'Price per hour',
|
||||
})
|
||||
case ParkingPricePeriods.day:
|
||||
return intl.formatMessage({
|
||||
defaultMessage: 'Price per day',
|
||||
})
|
||||
case ParkingPricePeriods.night:
|
||||
return intl.formatMessage({
|
||||
defaultMessage: 'Price per night',
|
||||
})
|
||||
case ParkingPricePeriods.allDay:
|
||||
return intl.formatMessage({
|
||||
defaultMessage: 'Price per 24 hours',
|
||||
})
|
||||
default:
|
||||
return period
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
'use client'
|
||||
|
||||
import { useIntl } from 'react-intl'
|
||||
|
||||
import { Divider } from '../Divider'
|
||||
import { MaterialIcon } from '../Icons/MaterialIcon'
|
||||
import { Typography } from '../Typography'
|
||||
import ButtonLink from '../ButtonLink'
|
||||
import ParkingList from './ParkingList'
|
||||
import ParkingPrices from './ParkingPrices'
|
||||
|
||||
import styles from './parkingInformation.module.css'
|
||||
|
||||
import type { Parking } from './parkingInformationTypes'
|
||||
|
||||
type ParkingInformationProps = {
|
||||
parking: Parking
|
||||
showExternalParkingButton?: boolean
|
||||
}
|
||||
|
||||
export default function ParkingInformation({
|
||||
parking,
|
||||
showExternalParkingButton = true,
|
||||
}: ParkingInformationProps) {
|
||||
const intl = useIntl()
|
||||
const title = `${parking.type}${parking.name ? ` (${parking.name})` : ''}`
|
||||
|
||||
return (
|
||||
<div className={styles.parkingInformation}>
|
||||
<div className={styles.list}>
|
||||
<Typography variant="Title/Subtitle/md">
|
||||
<h4 className={styles.heading}>{title}</h4>
|
||||
</Typography>
|
||||
<ParkingList
|
||||
numberOfChargingSpaces={parking.numberOfChargingSpaces}
|
||||
canMakeReservation={parking.canMakeReservation}
|
||||
numberOfParkingSpots={parking.numberOfParkingSpots}
|
||||
distanceToHotel={parking.distanceToHotel}
|
||||
address={parking.address}
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.prices}>
|
||||
<Typography variant="Body/Paragraph/mdBold">
|
||||
<h5 className={styles.heading}>
|
||||
{intl.formatMessage({ defaultMessage: 'Prices' })}
|
||||
</h5>
|
||||
</Typography>
|
||||
<div className={styles.priceWrapper}>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h6 className={styles.priceHeading}>
|
||||
{intl.formatMessage({ defaultMessage: 'Weekday prices' })}
|
||||
</h6>
|
||||
</Typography>
|
||||
<Divider />
|
||||
{parking.pricing.localCurrency ? (
|
||||
<ParkingPrices
|
||||
currency={parking.pricing.localCurrency.currency}
|
||||
freeParking={parking.pricing.freeParking}
|
||||
pricing={parking.pricing.localCurrency.ordinary}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
<div className={styles.priceWrapper}>
|
||||
<Typography variant="Title/Overline/sm">
|
||||
<h6 className={styles.priceHeading}>
|
||||
{intl.formatMessage({ defaultMessage: 'Weekend prices' })}
|
||||
</h6>
|
||||
</Typography>
|
||||
<Divider />
|
||||
{parking.pricing.localCurrency ? (
|
||||
<ParkingPrices
|
||||
currency={parking.pricing.localCurrency.currency}
|
||||
freeParking={parking.pricing.freeParking}
|
||||
pricing={parking.pricing.localCurrency.weekend}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
{parking.externalParkingUrl && showExternalParkingButton && (
|
||||
<ButtonLink
|
||||
typography="Body/Paragraph/mdBold"
|
||||
href={parking.externalParkingUrl}
|
||||
target="_blank"
|
||||
>
|
||||
{intl.formatMessage({ defaultMessage: 'Book parking' })}
|
||||
<MaterialIcon icon="open_in_new" color="CurrentColor" />
|
||||
</ButtonLink>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
.parkingInformation {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x3);
|
||||
}
|
||||
|
||||
.list,
|
||||
.prices {
|
||||
display: grid;
|
||||
gap: var(--Spacing-x-one-and-half);
|
||||
}
|
||||
|
||||
.priceWrapper {
|
||||
background-color: var(--Base-Surface-Subtle-Normal);
|
||||
border-radius: var(--Corner-radius-md);
|
||||
padding: var(--Spacing-x2) var(--Spacing-x3);
|
||||
display: grid;
|
||||
gap: var(--Spacing-x1);
|
||||
}
|
||||
|
||||
.heading {
|
||||
color: var(--Text-Default);
|
||||
}
|
||||
|
||||
.priceHeading {
|
||||
color: var(--Text-Secondary);
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
type Price = {
|
||||
amount: number
|
||||
endTime: string
|
||||
period: string
|
||||
startTime: string
|
||||
}
|
||||
|
||||
type CurrencyPrice = {
|
||||
currency: string
|
||||
weekend: Price[]
|
||||
ordinary: Price[]
|
||||
}
|
||||
|
||||
export type Parking = {
|
||||
address: string
|
||||
canMakeReservation: boolean
|
||||
distanceToHotel: number
|
||||
externalParkingUrl: string
|
||||
name: string
|
||||
numberOfChargingSpaces: number
|
||||
numberOfParkingSpots: number
|
||||
type: string
|
||||
pricing: {
|
||||
freeParking: boolean
|
||||
paymentType: string
|
||||
localCurrency?: CurrencyPrice | null
|
||||
}
|
||||
}
|
||||
|
||||
export enum ParkingPricePeriods {
|
||||
allDay = 'AllDay',
|
||||
hour = 'Hour',
|
||||
day = 'Day',
|
||||
night = 'Night',
|
||||
}
|
||||
@@ -41,6 +41,7 @@
|
||||
"./RegularRateCard": "./lib/components/RateCard/Regular/index.tsx",
|
||||
"./CampaignRateCard": "./lib/components/RateCard/Campaign/index.tsx",
|
||||
"./CodeRateCard": "./lib/components/RateCard/Code/index.tsx",
|
||||
"./ParkingInformation": "./lib/components/ParkingInformation/index.tsx",
|
||||
"./PointsRateCard": "./lib/components/RateCard/Points/index.tsx",
|
||||
"./Preamble": "./lib/components/Preamble/index.tsx",
|
||||
"./NoRateAvailableCard": "./lib/components/RateCard/NoRateAvailable/index.tsx",
|
||||
@@ -161,6 +162,9 @@
|
||||
"prepare": "husky && yarn run build",
|
||||
"check-types": "tsc --noEmit"
|
||||
},
|
||||
"dependencies": {
|
||||
"@scandic-hotels/common": "workspace:*"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@internationalized/date": "^3.8.0",
|
||||
"@radix-ui/react-slot": "^1.2.2",
|
||||
@@ -170,6 +174,7 @@
|
||||
"react-dom": "^19.1.0",
|
||||
"react-hook-form": "^7.56.2",
|
||||
"react-international-phone": "^4.5.0",
|
||||
"react-intl": "^7",
|
||||
"usehooks-ts": "3.1.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -47,6 +47,8 @@ export default defineConfig({
|
||||
'react/jsx-runtime',
|
||||
'react-aria-components',
|
||||
'react-hook-form',
|
||||
'react-intl',
|
||||
'next',
|
||||
],
|
||||
onwarn(warning, defaultHandler) {
|
||||
if (
|
||||
|
||||
Reference in New Issue
Block a user