Merged in monorepo-step-1 (pull request #1080)

Migrate to a monorepo setup - step 1

* Move web to subfolder /apps/scandic-web

* Yarn + transitive deps

- Move to yarn
- design-system package removed for now since yarn doesn't
support the parameter for token (ie project currently broken)
- Add missing transitive dependencies as Yarn otherwise
prevents these imports
- VS Code doesn't pick up TS path aliases unless you open
/apps/scandic-web instead of root (will be fixed with monorepo)

* Pin framer-motion to temporarily fix typing issue

https://github.com/adobe/react-spectrum/issues/7494

* Pin zod to avoid typ error

There seems to have been a breaking change in the types
returned by zod where error is now returned as undefined
instead of missing in the type. We should just handle this
but to avoid merge conflicts just pin the dependency for
now.

* Pin react-intl version

Pin version of react-intl to avoid tiny type issue where formatMessage
does not accept a generic any more. This will be fixed in a future
commit, but to avoid merge conflicts just pin for now.

* Pin typescript version

Temporarily pin version as newer versions as stricter and results in
a type error. Will be fixed in future commit after merge.

* Setup workspaces

* Add design-system as a monorepo package

* Remove unused env var DESIGN_SYSTEM_ACCESS_TOKEN

* Fix husky for monorepo setup

* Update netlify.toml

* Add lint script to root package.json

* Add stub readme

* Fix react-intl formatMessage types

* Test netlify.toml in root

* Remove root toml

* Update netlify.toml publish path

* Remove package-lock.json

* Update build for branch/preview builds


Approved-by: Linus Flood
This commit is contained in:
Anton Gunnarsson
2025-02-26 10:36:17 +00:00
committed by Linus Flood
parent 667cab6fb6
commit 80100e7631
2731 changed files with 30986 additions and 23708 deletions

View File

@@ -0,0 +1,24 @@
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import Input from "@/components/TempDesignSystem/Form/Input"
import type { BookingWidgetSchema } from "@/types/components/bookingWidget"
export default function TabletCodeInput({
updateValue,
}: {
updateValue: (val: string) => void
}) {
const intl = useIntl()
const { register } = useFormContext<BookingWidgetSchema>()
return (
<Input
label={intl.formatMessage({ id: "Add code" })}
{...register("bookingCode.value", {
onChange: (e) => updateValue(e.target.value),
})}
autoComplete="off"
/>
)
}

View File

@@ -0,0 +1,77 @@
.container {
position: relative;
}
.bookingCodeLabel {
display: flex;
align-items: center;
gap: var(--Spacing-x-half);
position: relative;
}
.error {
display: flex;
gap: var(--Spacing-x-half);
}
.errorIcon {
min-width: 20px;
}
.bookingCodeRemember,
.bookingCodeRememberVisible {
display: none;
gap: var(--Spacing-x1);
}
.bookingCodeRememberVisible {
display: flex;
position: absolute;
top: calc(100% + 16px);
width: 100%;
}
@media screen and (max-width: 767px) {
.hideOnMobile {
display: none;
}
}
@media screen and (min-width: 768px) {
.bookingCodeRememberVisible {
align-items: center;
background: var(--Base-Surface-Primary-light-Normal);
justify-content: space-between;
border-radius: var(--Spacing-x-one-and-half);
}
}
@media screen and (min-width: 768px) and (max-width: 1367px) {
.container {
display: flex;
gap: var(--Spacing-x1);
}
.codePopover {
background: var(--Base-Surface-Primary-light-Normal);
border-radius: var(--Spacing-x-one-and-half);
box-shadow: 0px 4px 24px 0px rgba(0, 0, 0, 0.05);
padding: var(--Spacing-x2);
width: 320px;
}
.popover {
display: grid;
gap: var(--Spacing-x2);
}
.bookingCodeRememberVisible {
position: static;
}
}
@media screen and (min-width: 1367px) {
.bookingCodeRememberVisible {
padding: var(--Spacing-x2);
width: 320px;
top: calc(100% + 24px);
left: calc(0% - var(--Spacing-x-one-and-half));
}
}

View File

@@ -0,0 +1,259 @@
import { useCallback, useEffect, useRef, useState } from "react"
import { Dialog, DialogTrigger, Popover } from "react-aria-components"
import { useFormContext } from "react-hook-form"
import { useIntl } from "react-intl"
import { useMediaQuery } from "usehooks-ts"
import { ErrorCircleIcon, InfoCircleIcon } from "@/components/Icons"
import Modal from "@/components/Modal"
import Button from "@/components/TempDesignSystem/Button"
import Checkbox from "@/components/TempDesignSystem/Form/Checkbox"
import Switch from "@/components/TempDesignSystem/Form/Switch"
import Body from "@/components/TempDesignSystem/Text/Body"
import Caption from "@/components/TempDesignSystem/Text/Caption"
import Input from "../Input"
import TabletCodeInput from "./TabletCodeInput"
import styles from "./booking-code.module.css"
import type {
BookingCodeSchema,
BookingWidgetSchema,
} from "@/types/components/bookingWidget"
export default function BookingCode() {
const intl = useIntl()
const checkIsTablet = useMediaQuery(
"(min-width: 768px) and (max-width: 1367px)"
)
const checkIsDesktop = useMediaQuery("(min-width: 1367px)")
const [isTablet, setIsTablet] = useState(false)
const [isDesktop, setIsDesktop] = useState(false)
const {
setValue,
formState: { errors },
getValues,
register,
} = useFormContext<BookingWidgetSchema>()
const bookingCode: BookingCodeSchema = getValues("bookingCode")
const [isOpen, setIsOpen] = useState(!!bookingCode?.value)
const [showRemember, setShowRemember] = useState(false)
const [showRememberMobile, setShowRememberMobile] = useState(false)
const codeError = errors["bookingCode"]?.value
const codeVoucher = intl.formatMessage({ id: "Code / Voucher" })
const addCode = intl.formatMessage({ id: "Add code" })
const ref = useRef<HTMLDivElement | null>(null)
function updateBookingCodeFormValue(value: string) {
setValue("bookingCode.value", value, { shouldValidate: true })
}
function toggleModal(isOpen: boolean) {
if (!isOpen && !bookingCode?.value) {
setValue("bookingCode.flag", false)
setIsOpen(isOpen)
} else if (!codeError || isOpen) {
setIsOpen(isOpen)
if (isOpen || bookingCode?.value) {
setValue("bookingCode.flag", true)
}
}
}
const closeIfOutside = useCallback(
(target: HTMLElement) => {
if (ref.current && target && !ref.current.contains(target)) {
setShowRemember(false)
}
},
[setShowRemember, ref]
)
function showRememberCheck() {
setShowRemember(true)
}
useEffect(() => {
setIsTablet(checkIsTablet)
}, [checkIsTablet])
useEffect(() => {
setIsDesktop(checkIsDesktop)
}, [checkIsDesktop])
const isRememberMobileVisible =
!isDesktop && (showRemember || !!bookingCode?.remember)
useEffect(() => {
setShowRememberMobile(isRememberMobileVisible)
}, [isRememberMobileVisible])
useEffect(() => {
function handleClickOutside(evt: Event) {
if (showRemember) {
const target = evt.target as HTMLElement
closeIfOutside(target)
}
}
document.body.addEventListener("click", handleClickOutside)
return () => {
document.body.removeEventListener("click", handleClickOutside)
}
}, [closeIfOutside, showRemember])
return isTablet ? (
<div className={styles.container}>
<DialogTrigger isOpen={isOpen} onOpenChange={toggleModal}>
<Button type="button" intent="text">
<Checkbox
checked={!!bookingCode?.value}
{...register("bookingCode.flag", {
onChange: function () {
if (bookingCode?.value || isOpen) {
setValue("bookingCode.flag", true)
}
},
})}
>
<Caption color="red" type="bold" asChild>
<span>{codeVoucher}</span>
</Caption>
</Checkbox>
</Button>
<Popover
className={styles.codePopover}
placement="bottom start"
offset={36}
>
<Dialog>
{({ close }) => (
<div className={styles.popover}>
<TabletCodeInput updateValue={updateBookingCodeFormValue} />
<div className={styles.bookingCodeRememberVisible}>
<CodeRemember
bookingCodeValue={bookingCode?.value}
onApplyClick={close}
/>
</div>
</div>
)}
</Dialog>
</Popover>
</DialogTrigger>
<CodeRulesModal />
</div>
) : (
<div
className={styles.container}
ref={ref}
onFocus={showRememberCheck}
onBlur={(e) => closeIfOutside(e.nativeEvent.relatedTarget as HTMLElement)}
>
<div className={styles.bookingCodeLabel}>
<Caption
color={showRemember ? "uiTextActive" : "red"}
type="bold"
asChild
>
<span>{codeVoucher}</span>
</Caption>
<CodeRulesModal />
</div>
<Input
className="input"
type="search"
placeholder={addCode}
name="bookingCode.value"
id="booking-code"
onChange={(event) => updateBookingCodeFormValue(event.target.value)}
defaultValue={bookingCode?.value}
autoComplete="off"
/>
{codeError?.message ? (
<Caption color="red" className={styles.error}>
<ErrorCircleIcon color="red" className={styles.errorIcon} />
{intl.formatMessage({ id: codeError.message })}
</Caption>
) : null}
{isDesktop ? (
<div
className={
showRemember
? styles.bookingCodeRememberVisible
: styles.bookingCodeRemember
}
>
<CodeRemember
bookingCodeValue={bookingCode?.value}
onApplyClick={() => setShowRemember(false)}
/>
</div>
) : (
<div
className={
showRememberMobile
? styles.bookingCodeRememberVisible
: styles.bookingCodeRemember
}
>
<Switch name="bookingCode.remember" className="mobile-switch">
<Caption asChild>
<span>{intl.formatMessage({ id: "Remember code" })}</span>
</Caption>
</Switch>
</div>
)}
</div>
)
}
type CodeRememberProps = {
bookingCodeValue: string | undefined
onApplyClick: () => void
}
function CodeRulesModal() {
const intl = useIntl()
const codeVoucher = intl.formatMessage({ id: "Code / Voucher" })
const bookingCodeTooltipText = intl.formatMessage({
id: "If you're booking a promotional offer or a Corporate negotiated rate you'll need a special booking code. Don't use any special characters such as (.) (,) (-) (:). If you would like to make a booking with code VOF, please call us +46 8 517 517 20.Save your booking code for the next time you visit the page by ticking the box “Remember”. Don't tick the box if you're using a public computer to avoid unauthorized access to your booking code.",
})
return (
<Modal
trigger={
<Button intent="text">
<InfoCircleIcon color="uiTextPlaceholder" height={20} width={20} />
</Button>
}
title={codeVoucher}
>
<Body color="uiTextHighContrast">{bookingCodeTooltipText}</Body>
</Modal>
)
}
function CodeRemember({ bookingCodeValue, onApplyClick }: CodeRememberProps) {
const intl = useIntl()
return (
<>
<Checkbox name="bookingCode.remember">
<Caption asChild>
<span>{intl.formatMessage({ id: "Remember code" })}</span>
</Caption>
</Checkbox>
{bookingCodeValue ? (
<Button
size="small"
className={styles.hideOnMobile}
intent="secondary"
type="button"
onClick={onApplyClick}
>
{intl.formatMessage({ id: "Apply" })}
</Button>
) : null}
</>
)
}