diff --git a/packages/visual-editor/locales/platform/cs/visual-editor.json b/packages/visual-editor/locales/platform/cs/visual-editor.json index 2d65cb42dc..a220acb94d 100644 --- a/packages/visual-editor/locales/platform/cs/visual-editor.json +++ b/packages/visual-editor/locales/platform/cs/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Základní informace", "other": "OSTATNÍ", - "standardSections": "Standardní sekce" + "standardSections": "Standardní sekce", + "yetiSections": "Sekce Yeti" }, "change": "Přeměna", "chooseImage": "Vyberte obrázek", diff --git a/packages/visual-editor/locales/platform/da/visual-editor.json b/packages/visual-editor/locales/platform/da/visual-editor.json index 66eae96cde..76e836ad8e 100644 --- a/packages/visual-editor/locales/platform/da/visual-editor.json +++ b/packages/visual-editor/locales/platform/da/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Kerneoplysninger", "other": "ANDRE", - "standardSections": "Standard sektioner" + "standardSections": "Standard sektioner", + "yetiSections": "Yeti sektioner" }, "change": "Forandring", "chooseImage": "Vælg billede", diff --git a/packages/visual-editor/locales/platform/de/visual-editor.json b/packages/visual-editor/locales/platform/de/visual-editor.json index 540413c7df..4874ad06f8 100644 --- a/packages/visual-editor/locales/platform/de/visual-editor.json +++ b/packages/visual-editor/locales/platform/de/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Basisinformationen", "other": "Sonstiges", - "standardSections": "Standardmodule" + "standardSections": "Standardmodule", + "yetiSections": "Yeti-Abschnitte" }, "change": "Ändern", "chooseImage": "Bild auswählen", diff --git a/packages/visual-editor/locales/platform/en-GB/visual-editor.json b/packages/visual-editor/locales/platform/en-GB/visual-editor.json index b4827b64d1..de6060e53e 100644 --- a/packages/visual-editor/locales/platform/en-GB/visual-editor.json +++ b/packages/visual-editor/locales/platform/en-GB/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Core Information", "other": "OTHER", - "standardSections": "Standard Sections" + "standardSections": "Standard Sections", + "yetiSections": "Yeti Sections" }, "change": "Change", "chooseImage": "Choose Image", diff --git a/packages/visual-editor/locales/platform/en/visual-editor.json b/packages/visual-editor/locales/platform/en/visual-editor.json index 79c90dbcca..f8b63845b2 100644 --- a/packages/visual-editor/locales/platform/en/visual-editor.json +++ b/packages/visual-editor/locales/platform/en/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Core Information", "other": "Other", - "standardSections": "Standard Sections" + "standardSections": "Standard Sections", + "yetiSections": "Yeti Sections" }, "change": "Change", "chooseImage": "Choose Image", diff --git a/packages/visual-editor/locales/platform/es/visual-editor.json b/packages/visual-editor/locales/platform/es/visual-editor.json index 8bee9b4b1c..31cb26ffae 100644 --- a/packages/visual-editor/locales/platform/es/visual-editor.json +++ b/packages/visual-editor/locales/platform/es/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Información básica", "other": "OTRO", - "standardSections": "Secciones estándar" + "standardSections": "Secciones estándar", + "yetiSections": "Secciones Yeti" }, "change": "Cambiar", "chooseImage": "Elija imagen", diff --git a/packages/visual-editor/locales/platform/et/visual-editor.json b/packages/visual-editor/locales/platform/et/visual-editor.json index 8bd0859e07..1036ed4535 100644 --- a/packages/visual-editor/locales/platform/et/visual-editor.json +++ b/packages/visual-editor/locales/platform/et/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Põhiteave", "other": "Teine", - "standardSections": "Standardsektsioonid" + "standardSections": "Standardsektsioonid", + "yetiSections": "Yeti sektsioonid" }, "change": "Vahetus", "chooseImage": "Valige pilt", diff --git a/packages/visual-editor/locales/platform/fi/visual-editor.json b/packages/visual-editor/locales/platform/fi/visual-editor.json index 14cca51c44..8de644daae 100644 --- a/packages/visual-editor/locales/platform/fi/visual-editor.json +++ b/packages/visual-editor/locales/platform/fi/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Perustiedot", "other": "Muut", - "standardSections": "Vakiojaksot" + "standardSections": "Vakiojaksot", + "yetiSections": "Yeti-osastot" }, "change": "Muuttaa", "chooseImage": "Valitse kuva", diff --git a/packages/visual-editor/locales/platform/fr/visual-editor.json b/packages/visual-editor/locales/platform/fr/visual-editor.json index bf3feabd72..864718cfa8 100644 --- a/packages/visual-editor/locales/platform/fr/visual-editor.json +++ b/packages/visual-editor/locales/platform/fr/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Informations de base", "other": "AUTRE", - "standardSections": "Sections standards" + "standardSections": "Sections standards", + "yetiSections": "Sections Yéti" }, "change": "Changement", "chooseImage": "Choisir l'image", diff --git a/packages/visual-editor/locales/platform/hr/visual-editor.json b/packages/visual-editor/locales/platform/hr/visual-editor.json index be50762b9d..64d8106832 100644 --- a/packages/visual-editor/locales/platform/hr/visual-editor.json +++ b/packages/visual-editor/locales/platform/hr/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Osnovne informacije", "other": "Drugi", - "standardSections": "Standardni odjeljci" + "standardSections": "Standardni odjeljci", + "yetiSections": "Yeti sekcije" }, "change": "Promijeniti", "chooseImage": "Odaberite sliku", diff --git a/packages/visual-editor/locales/platform/hu/visual-editor.json b/packages/visual-editor/locales/platform/hu/visual-editor.json index 40db34d54b..33cf098876 100644 --- a/packages/visual-editor/locales/platform/hu/visual-editor.json +++ b/packages/visual-editor/locales/platform/hu/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Alapvető információk", "other": "MÁS", - "standardSections": "Szabványos szakaszok" + "standardSections": "Szabványos szakaszok", + "yetiSections": "Yeti szakaszok" }, "change": "Változás", "chooseImage": "Válassza ki a képet", diff --git a/packages/visual-editor/locales/platform/it/visual-editor.json b/packages/visual-editor/locales/platform/it/visual-editor.json index 4928602f9b..97700fcd4e 100644 --- a/packages/visual-editor/locales/platform/it/visual-editor.json +++ b/packages/visual-editor/locales/platform/it/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Informazioni fondamentali", "other": "ALTRO", - "standardSections": "Sezioni standard" + "standardSections": "Sezioni standard", + "yetiSections": "Sezioni Yeti" }, "change": "Modifica", "chooseImage": "Scegli l'immagine", diff --git a/packages/visual-editor/locales/platform/ja/visual-editor.json b/packages/visual-editor/locales/platform/ja/visual-editor.json index 8e056681b0..2743bf3aa4 100644 --- a/packages/visual-editor/locales/platform/ja/visual-editor.json +++ b/packages/visual-editor/locales/platform/ja/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "コア情報", "other": "他の", - "standardSections": "標準セクション" + "standardSections": "標準セクション", + "yetiSections": "イエティセクション" }, "change": "変化", "chooseImage": "画像を選択します", diff --git a/packages/visual-editor/locales/platform/lt/visual-editor.json b/packages/visual-editor/locales/platform/lt/visual-editor.json index dbf0bdc65b..93c8c538c2 100644 --- a/packages/visual-editor/locales/platform/lt/visual-editor.json +++ b/packages/visual-editor/locales/platform/lt/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Pagrindinė informacija", "other": "Kita", - "standardSections": "Standartiniai skyriai" + "standardSections": "Standartiniai skyriai", + "yetiSections": "Yeti skyriai" }, "change": "Pakeisti", "chooseImage": "Pasirinkite vaizdą", diff --git a/packages/visual-editor/locales/platform/lv/visual-editor.json b/packages/visual-editor/locales/platform/lv/visual-editor.json index 8d9a17351b..e4ede9120b 100644 --- a/packages/visual-editor/locales/platform/lv/visual-editor.json +++ b/packages/visual-editor/locales/platform/lv/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Pamatinformācija", "other": "Cits", - "standardSections": "Standarta sadaļas" + "standardSections": "Standarta sadaļas", + "yetiSections": "Yeti sadaļas" }, "change": "Mainīt", "chooseImage": "Izvēlieties attēlu", diff --git a/packages/visual-editor/locales/platform/nb/visual-editor.json b/packages/visual-editor/locales/platform/nb/visual-editor.json index 0e9ea5e18d..5279ed34be 100644 --- a/packages/visual-editor/locales/platform/nb/visual-editor.json +++ b/packages/visual-editor/locales/platform/nb/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Kjerneinformasjon", "other": "ANNEN", - "standardSections": "Standard seksjoner" + "standardSections": "Standard seksjoner", + "yetiSections": "Yeti-seksjoner" }, "change": "Endre", "chooseImage": "Velg bilde", diff --git a/packages/visual-editor/locales/platform/nl/visual-editor.json b/packages/visual-editor/locales/platform/nl/visual-editor.json index 4bdb0ea119..b69553901a 100644 --- a/packages/visual-editor/locales/platform/nl/visual-editor.json +++ b/packages/visual-editor/locales/platform/nl/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Kerninformatie", "other": "ANDER", - "standardSections": "Standaard secties" + "standardSections": "Standaard secties", + "yetiSections": "Yeti-secties" }, "change": "Wijziging", "chooseImage": "Kies afbeelding", diff --git a/packages/visual-editor/locales/platform/pl/visual-editor.json b/packages/visual-editor/locales/platform/pl/visual-editor.json index 6cca82a48b..2cb19605fb 100644 --- a/packages/visual-editor/locales/platform/pl/visual-editor.json +++ b/packages/visual-editor/locales/platform/pl/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Informacje podstawowe", "other": "INNY", - "standardSections": "Sekcje standardowe" + "standardSections": "Sekcje standardowe", + "yetiSections": "Sekcje Yeti" }, "change": "Zmiana", "chooseImage": "Wybierz obraz", diff --git a/packages/visual-editor/locales/platform/pt/visual-editor.json b/packages/visual-editor/locales/platform/pt/visual-editor.json index 9d695f21cd..e23ec31600 100644 --- a/packages/visual-editor/locales/platform/pt/visual-editor.json +++ b/packages/visual-editor/locales/platform/pt/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Informações essenciais", "other": "OUTRO", - "standardSections": "Seções padrão" + "standardSections": "Seções padrão", + "yetiSections": "Seções Yeti" }, "change": "Mudar", "chooseImage": "Escolha a imagem", diff --git a/packages/visual-editor/locales/platform/ro/visual-editor.json b/packages/visual-editor/locales/platform/ro/visual-editor.json index beb0f5c844..e95fc1e077 100644 --- a/packages/visual-editor/locales/platform/ro/visual-editor.json +++ b/packages/visual-editor/locales/platform/ro/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Informații de bază", "other": "ALTE", - "standardSections": "Secțiuni standard" + "standardSections": "Secțiuni standard", + "yetiSections": "Secțiuni Yeti" }, "change": "Schimba", "chooseImage": "Alege Imagine", diff --git a/packages/visual-editor/locales/platform/sk/visual-editor.json b/packages/visual-editor/locales/platform/sk/visual-editor.json index 7f6c3770a0..ddcd7bb373 100644 --- a/packages/visual-editor/locales/platform/sk/visual-editor.json +++ b/packages/visual-editor/locales/platform/sk/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Základné informácie", "other": "Druhý", - "standardSections": "Štandardné sekcie" + "standardSections": "Štandardné sekcie", + "yetiSections": "Sekcie Yeti" }, "change": "Zmena", "chooseImage": "Vyberte obrázok", diff --git a/packages/visual-editor/locales/platform/sv/visual-editor.json b/packages/visual-editor/locales/platform/sv/visual-editor.json index fb97c8d834..5ddc69fe26 100644 --- a/packages/visual-editor/locales/platform/sv/visual-editor.json +++ b/packages/visual-editor/locales/platform/sv/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Kärninformation", "other": "ANDRA", - "standardSections": "Standardsektioner" + "standardSections": "Standardsektioner", + "yetiSections": "Yeti-sektioner" }, "change": "Ändra", "chooseImage": "Välj bild", diff --git a/packages/visual-editor/locales/platform/tr/visual-editor.json b/packages/visual-editor/locales/platform/tr/visual-editor.json index 7ec127d08f..01227210c2 100644 --- a/packages/visual-editor/locales/platform/tr/visual-editor.json +++ b/packages/visual-editor/locales/platform/tr/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "Temel Bilgiler", "other": "DİĞER", - "standardSections": "Standart Bölümler" + "standardSections": "Standart Bölümler", + "yetiSections": "Yeti Bölümleri" }, "change": "Değiştirmek", "chooseImage": "Resmi Seçin", diff --git a/packages/visual-editor/locales/platform/zh-TW/visual-editor.json b/packages/visual-editor/locales/platform/zh-TW/visual-editor.json index 18057976ef..ea96bc7b12 100644 --- a/packages/visual-editor/locales/platform/zh-TW/visual-editor.json +++ b/packages/visual-editor/locales/platform/zh-TW/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "核心信息", "other": "其他", - "standardSections": "標準型材" + "standardSections": "標準型材", + "yetiSections": "雪人部分" }, "change": "改變", "chooseImage": "選擇圖像", diff --git a/packages/visual-editor/locales/platform/zh/visual-editor.json b/packages/visual-editor/locales/platform/zh/visual-editor.json index b1346a5972..a4dbe406b3 100644 --- a/packages/visual-editor/locales/platform/zh/visual-editor.json +++ b/packages/visual-editor/locales/platform/zh/visual-editor.json @@ -34,7 +34,8 @@ "categories": { "coreInformation": "核心信息", "other": "其他", - "standardSections": "标准区块" + "standardSections": "标准区块", + "yetiSections": "雪人部分" }, "change": "改变", "chooseImage": "选择图像", diff --git a/packages/visual-editor/src/components/categories/YetiSectionsCategory.tsx b/packages/visual-editor/src/components/categories/YetiSectionsCategory.tsx new file mode 100644 index 0000000000..02a8a434d9 --- /dev/null +++ b/packages/visual-editor/src/components/categories/YetiSectionsCategory.tsx @@ -0,0 +1,52 @@ +import { + YetiExploreCarouselSection, + YetiExploreCarouselSectionProps, +} from "../custom/yeti/components/YetiExploreCarouselSection.tsx"; +import { + YetiFaqSection, + YetiFaqSectionProps, +} from "../custom/yeti/components/YetiFaqSection.tsx"; +import { + YetiFooterSection, + YetiFooterSectionProps, +} from "../custom/yeti/components/YetiFooterSection.tsx"; +import { + YetiHeaderSection, + YetiHeaderSectionProps, +} from "../custom/yeti/components/YetiHeaderSection.tsx"; +import { + YetiLocationHeroSection, + YetiLocationHeroSectionProps, +} from "../custom/yeti/components/YetiLocationHeroSection.tsx"; +import { + YetiPromoBannerSection, + YetiPromoBannerSectionProps, +} from "../custom/yeti/components/YetiPromoBannerSection.tsx"; +import { + YetiStoreInfoSection, + YetiStoreInfoSectionProps, +} from "../custom/yeti/components/YetiStoreInfoSection.tsx"; + +export interface YetiSectionsCategoryProps { + YetiHeaderSection: YetiHeaderSectionProps; + YetiLocationHeroSection: YetiLocationHeroSectionProps; + YetiStoreInfoSection: YetiStoreInfoSectionProps; + YetiPromoBannerSection: YetiPromoBannerSectionProps; + YetiExploreCarouselSection: YetiExploreCarouselSectionProps; + YetiFaqSection: YetiFaqSectionProps; + YetiFooterSection: YetiFooterSectionProps; +} + +export const YetiSectionsCategoryComponents = { + YetiHeaderSection, + YetiLocationHeroSection, + YetiStoreInfoSection, + YetiPromoBannerSection, + YetiExploreCarouselSection, + YetiFaqSection, + YetiFooterSection, +}; + +export const YetiSectionsCategory = Object.keys( + YetiSectionsCategoryComponents +) as (keyof YetiSectionsCategoryProps)[]; diff --git a/packages/visual-editor/src/components/categories/YetiSlotsCategory.tsx b/packages/visual-editor/src/components/categories/YetiSlotsCategory.tsx new file mode 100644 index 0000000000..34f5e8af4b --- /dev/null +++ b/packages/visual-editor/src/components/categories/YetiSlotsCategory.tsx @@ -0,0 +1,124 @@ +import { + YetiBodyCopySlot, + YetiBodyCopySlotProps, +} from "../custom/yeti/components/YetiBodyCopySlot.tsx"; +import { + YetiBrandSlot, + YetiBrandSlotProps, +} from "../custom/yeti/components/YetiBrandSlot.tsx"; +import { + YetiExploreCardsSlot, + YetiExploreCardsSlotProps, +} from "../custom/yeti/components/YetiExploreCardsSlot.tsx"; +import { + YetiFaqListSlot, + YetiFaqListSlotProps, +} from "../custom/yeti/components/YetiFaqListSlot.tsx"; +import { + YetiFooterLayoutSlot, + YetiFooterLayoutSlotProps, +} from "../custom/yeti/components/YetiFooterLayoutSlot.tsx"; +import { + YetiFooterLegalSlot, + YetiFooterLegalSlotProps, +} from "../custom/yeti/components/YetiFooterLegalSlot.tsx"; +import { + YetiFooterLinksColumnSlot, + YetiFooterLinksColumnSlotProps, +} from "../custom/yeti/components/YetiFooterLinksColumnSlot.tsx"; +import { + YetiFooterSignupSlot, + YetiFooterSignupSlotProps, +} from "../custom/yeti/components/YetiFooterSignupSlot.tsx"; +import { + YetiFooterSocialLinksSlot, + YetiFooterSocialLinksSlotProps, +} from "../custom/yeti/components/YetiFooterSocialLinksSlot.tsx"; +import { + YetiHeaderLayoutSlot, + YetiHeaderLayoutSlotProps, +} from "../custom/yeti/components/YetiHeaderLayoutSlot.tsx"; +import { + YetiHeroHeadingSlot, + YetiHeroHeadingSlotProps, +} from "../custom/yeti/components/YetiHeroHeadingSlot.tsx"; +import { + YetiHeroImageSlot, + YetiHeroImageSlotProps, +} from "../custom/yeti/components/YetiHeroImageSlot.tsx"; +import { + YetiHoursSlot, + YetiHoursSlotProps, +} from "../custom/yeti/components/YetiHoursSlot.tsx"; +import { + YetiLocationDetailsSlot, + YetiLocationDetailsSlotProps, +} from "../custom/yeti/components/YetiLocationDetailsSlot.tsx"; +import { + YetiMapSurfaceSlot, + YetiMapSurfaceSlotProps, +} from "../custom/yeti/components/YetiMapSurfaceSlot.tsx"; +import { + YetiNavLinksSlot, + YetiNavLinksSlotProps, +} from "../custom/yeti/components/YetiNavLinksSlot.tsx"; +import { + YetiParkingSlot, + YetiParkingSlotProps, +} from "../custom/yeti/components/YetiParkingSlot.tsx"; +import { + YetiPrimaryActionSlot, + YetiPrimaryActionSlotProps, +} from "../custom/yeti/components/YetiPrimaryActionSlot.tsx"; +import { + YetiSectionHeadingSlot, + YetiSectionHeadingSlotProps, +} from "../custom/yeti/components/YetiSectionHeadingSlot.tsx"; + +export interface YetiSlotsCategoryProps { + YetiHeaderLayoutSlot: YetiHeaderLayoutSlotProps; + YetiBrandSlot: YetiBrandSlotProps; + YetiNavLinksSlot: YetiNavLinksSlotProps; + YetiPrimaryActionSlot: YetiPrimaryActionSlotProps; + YetiHeroHeadingSlot: YetiHeroHeadingSlotProps; + YetiSectionHeadingSlot: YetiSectionHeadingSlotProps; + YetiHeroImageSlot: YetiHeroImageSlotProps; + YetiBodyCopySlot: YetiBodyCopySlotProps; + YetiHoursSlot: YetiHoursSlotProps; + YetiLocationDetailsSlot: YetiLocationDetailsSlotProps; + YetiParkingSlot: YetiParkingSlotProps; + YetiMapSurfaceSlot: YetiMapSurfaceSlotProps; + YetiExploreCardsSlot: YetiExploreCardsSlotProps; + YetiFaqListSlot: YetiFaqListSlotProps; + YetiFooterLayoutSlot: YetiFooterLayoutSlotProps; + YetiFooterSignupSlot: YetiFooterSignupSlotProps; + YetiFooterLinksColumnSlot: YetiFooterLinksColumnSlotProps; + YetiFooterSocialLinksSlot: YetiFooterSocialLinksSlotProps; + YetiFooterLegalSlot: YetiFooterLegalSlotProps; +} + +export const YetiSlotsCategoryComponents = { + YetiHeaderLayoutSlot, + YetiBrandSlot, + YetiNavLinksSlot, + YetiPrimaryActionSlot, + YetiHeroHeadingSlot, + YetiSectionHeadingSlot, + YetiHeroImageSlot, + YetiBodyCopySlot, + YetiHoursSlot, + YetiLocationDetailsSlot, + YetiParkingSlot, + YetiMapSurfaceSlot, + YetiExploreCardsSlot, + YetiFaqListSlot, + YetiFooterLayoutSlot, + YetiFooterSignupSlot, + YetiFooterLinksColumnSlot, + YetiFooterSocialLinksSlot, + YetiFooterLegalSlot, +}; + +export const YetiSlotsCategory = Object.keys( + YetiSlotsCategoryComponents +) as (keyof YetiSlotsCategoryProps)[]; diff --git a/packages/visual-editor/src/components/categories/index.ts b/packages/visual-editor/src/components/categories/index.ts index 2567410a34..61df53ae00 100644 --- a/packages/visual-editor/src/components/categories/index.ts +++ b/packages/visual-editor/src/components/categories/index.ts @@ -5,3 +5,5 @@ export * from "./OtherCategory.tsx"; export * from "./PageSectionCategory.tsx"; export * from "./DeprecatedCategory.tsx"; export * from "./SlotsCategory.tsx"; +export * from "./YetiSectionsCategory.tsx"; +export * from "./YetiSlotsCategory.tsx"; diff --git a/packages/visual-editor/src/components/configs/mainConfig.tsx b/packages/visual-editor/src/components/configs/mainConfig.tsx index eea77c427e..3516f3a6f5 100644 --- a/packages/visual-editor/src/components/configs/mainConfig.tsx +++ b/packages/visual-editor/src/components/configs/mainConfig.tsx @@ -25,20 +25,34 @@ import { SlotsCategoryComponents, SlotsCategoryProps, } from "../categories/SlotsCategory.tsx"; +import { + YetiSectionsCategory, + YetiSectionsCategoryComponents, + type YetiSectionsCategoryProps, +} from "../categories/YetiSectionsCategory.tsx"; +import { + YetiSlotsCategory, + YetiSlotsCategoryComponents, + type YetiSlotsCategoryProps, +} from "../categories/YetiSlotsCategory.tsx"; export interface MainConfigProps extends PageSectionCategoryProps, DeprecatedCategoryProps, OtherCategoryProps, AdvancedCoreInfoCategoryProps, - SlotsCategoryProps {} + SlotsCategoryProps, + YetiSectionsCategoryProps, + YetiSlotsCategoryProps {} const components: Config["components"] = { ...PageSectionCategoryComponents, + ...YetiSectionsCategoryComponents, ...DeprecatedCategoryComponents, ...OtherCategoryComponents, ...AdvancedCoreInfoCategoryComponents, ...SlotsCategoryComponents, + ...YetiSlotsCategoryComponents, }; // The config used for base entities (locations, financial professionals, etc.) @@ -49,6 +63,10 @@ export const mainConfig: Config = { title: pt("categories.standardSections", "Standard Sections"), components: PageSectionCategory, }, + yetiSections: { + title: pt("categories.yetiSections", "Yeti Sections"), + components: YetiSectionsCategory, + }, coreInformation: { title: pt("categories.coreInformation", "Core Information"), components: AdvancedCoreInfoCategory, @@ -61,6 +79,10 @@ export const mainConfig: Config = { components: SlotsCategory, visible: false, }, + yetiSlots: { + components: YetiSlotsCategory, + visible: false, + }, // deprecated components are hidden in the sidebar but still render if used in the page deprecatedComponents: { visible: false, diff --git a/packages/visual-editor/src/components/custom/yeti/atoms/YetiHeading.tsx b/packages/visual-editor/src/components/custom/yeti/atoms/YetiHeading.tsx new file mode 100644 index 0000000000..fae45bef82 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/atoms/YetiHeading.tsx @@ -0,0 +1,17 @@ +// @ts-nocheck +import * as React from "react"; + +export interface YetiHeadingProps { + level?: 1 | 2 | 3 | 4 | 5 | 6; + className?: string; + children: React.ReactNode; +} + +export const YetiHeading = ({ + level = 2, + className, + children, +}: YetiHeadingProps) => { + const Tag = `h${level}` as unknown as keyof JSX.IntrinsicElements; + return {children}; +}; diff --git a/packages/visual-editor/src/components/custom/yeti/atoms/YetiHoursTable.tsx b/packages/visual-editor/src/components/custom/yeti/atoms/YetiHoursTable.tsx new file mode 100644 index 0000000000..240263d8ab --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/atoms/YetiHoursTable.tsx @@ -0,0 +1,54 @@ +// @ts-nocheck +import { + HoursType, + HoursTable as SharedHoursTable, +} from "@yext/pages-components"; +import { useTranslation } from "react-i18next"; + +export interface YetiHoursTableProps { + hours: HoursType; + className?: string; + startOfWeek?: + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday" + | "today"; + collapseDays?: boolean; +} + +export const YetiHoursTable = ({ + hours, + className, + startOfWeek = "today", + collapseDays = false, +}: YetiHoursTableProps) => { + const { t, i18n } = useTranslation(); + + return ( + + ); +}; diff --git a/packages/visual-editor/src/components/custom/yeti/atoms/YetiParagraph.tsx b/packages/visual-editor/src/components/custom/yeti/atoms/YetiParagraph.tsx new file mode 100644 index 0000000000..e5851929b4 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/atoms/YetiParagraph.tsx @@ -0,0 +1,11 @@ +// @ts-nocheck +import * as React from "react"; + +export interface YetiParagraphProps { + className?: string; + children: React.ReactNode; +} + +export const YetiParagraph = ({ className, children }: YetiParagraphProps) => { + return

{children}

; +}; diff --git a/packages/visual-editor/src/components/custom/yeti/atoms/YetiSectionShell.tsx b/packages/visual-editor/src/components/custom/yeti/atoms/YetiSectionShell.tsx new file mode 100644 index 0000000000..44d7234834 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/atoms/YetiSectionShell.tsx @@ -0,0 +1,29 @@ +// @ts-nocheck +import * as React from "react"; + +export interface YetiSectionShellProps { + children: React.ReactNode; + backgroundClassName?: string; + className?: string; + contentClassName?: string; +} + +const join = (...parts: Array) => + parts.filter((part) => part && part.trim().length > 0).join(" "); + +export const YetiSectionShell = ({ + children, + backgroundClassName, + className, + contentClassName, +}: YetiSectionShellProps) => { + return ( +
+
+ {children} +
+
+ ); +}; diff --git a/packages/visual-editor/src/components/custom/yeti/atoms/defaults.ts b/packages/visual-editor/src/components/custom/yeti/atoms/defaults.ts new file mode 100644 index 0000000000..b19500f1f7 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/atoms/defaults.ts @@ -0,0 +1,45 @@ +// @ts-nocheck +import { TranslatableRichText, TranslatableString } from "../ve.ts"; + +export const toTranslatableString = (value: string): TranslatableString => ({ + en: value, + hasLocalizedValue: "true", +}); + +export const toTranslatableRichText = ( + value: string +): TranslatableRichText => ({ + en: { + html: `

${value}

`, + json: JSON.stringify({ + root: { + type: "root", + format: "", + indent: 0, + version: 1, + children: [ + { + type: "paragraph", + format: "", + indent: 0, + version: 1, + children: [ + { + detail: 0, + format: 0, + mode: "normal", + style: "", + text: value, + type: "text", + version: 1, + }, + ], + direction: "ltr", + }, + ], + direction: "ltr", + }, + }), + }, + hasLocalizedValue: "true", +}); diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiBodyCopySlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiBodyCopySlot.tsx new file mode 100644 index 0000000000..9cad5d99c4 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiBodyCopySlot.tsx @@ -0,0 +1,85 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiParagraph } from "../atoms/YetiParagraph.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiBodyCopySlotProps { + data: { + copy: TranslatableString; + }; + styles: { + align: "left" | "center"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + copy: YextField("Copy", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + align: YextField("Align", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + ], + }), + }, + }), +}; + +const YetiBodyCopySlotComponent: PuckComponent = ({ + data, + styles, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const copy = resolveComponentData(data.copy, i18n.language, streamDocument); + if (!copy) { + return null; + } + + return ( + + {copy} + + ); +}; + +export const defaultYetiBodyCopySlotProps: YetiBodyCopySlotProps = { + data: { + copy: toTranslatableString( + "Choose from 9 different fonts and 12 design galleries to make your drinkware your own." + ), + }, + styles: { + align: "left", + }, +}; + +export const YetiBodyCopySlot: ComponentConfig<{ + props: YetiBodyCopySlotProps; +}> = { + label: "Yeti Body Copy Slot", + fields, + defaultProps: defaultYetiBodyCopySlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiBrandSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiBrandSlot.tsx new file mode 100644 index 0000000000..b2958738f6 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiBrandSlot.tsx @@ -0,0 +1,106 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiBrandSlotProps { + data: { + logoLabel: TranslatableString; + logoHref: string; + utilityLabel: TranslatableString; + utilityHref: string; + }; + styles: { + showUtilityLink: boolean; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + logoLabel: YextField("Logo Label", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + logoHref: YextField("Logo Href", { type: "text" }), + utilityLabel: YextField("Utility Label", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + utilityHref: YextField("Utility Href", { type: "text" }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + showUtilityLink: YextField("Show Utility Link", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), +}; + +const YetiBrandSlotComponent: PuckComponent = ({ + data, + styles, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const logoLabel = resolveComponentData( + data.logoLabel, + i18n.language, + streamDocument + ); + const utilityLabel = resolveComponentData( + data.utilityLabel, + i18n.language, + streamDocument + ); + + return ( +
+ + {logoLabel} + + {styles.showUtilityLink && utilityLabel ? ( + + {utilityLabel} + + ) : null} +
+ ); +}; + +export const defaultYetiBrandSlotProps: YetiBrandSlotProps = { + data: { + logoLabel: toTranslatableString("YETI"), + logoHref: "https://www.yeti.com/", + utilityLabel: toTranslatableString("Find a Store"), + utilityHref: "https://www.yeti.com/yeti-store-locations.html", + }, + styles: { + showUtilityLink: true, + }, +}; + +export const YetiBrandSlot: ComponentConfig<{ props: YetiBrandSlotProps }> = { + label: "Yeti Brand Slot", + fields, + defaultProps: defaultYetiBrandSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiExploreCardsSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiExploreCardsSlot.tsx new file mode 100644 index 0000000000..ec120b5a2d --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiExploreCardsSlot.tsx @@ -0,0 +1,249 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextEntityField, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { YetiParagraph } from "../atoms/YetiParagraph.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +type ImageValue = { + url?: string; + width?: number; + height?: number; + image?: { + url?: string; + }; +}; + +const resolveImageUrl = (value: ImageValue | undefined): string => { + if (!value) { + return ""; + } + if (value.url) { + return value.url; + } + if (value.image?.url) { + return value.image.url; + } + return ""; +}; + +export interface YetiExploreCardsSlotProps { + data: { + cards: Array<{ + title: TranslatableString; + description: TranslatableString; + actionText: TranslatableString; + actionHref: string; + image: YextEntityField; + }>; + }; +} + +const defaultCard = { + title: toTranslatableString("Card Title"), + description: toTranslatableString("Card description."), + actionText: toTranslatableString("Learn more"), + actionHref: "#", + image: { + field: "", + constantValue: { + url: "", + width: 800, + height: 600, + }, + constantValueEnabled: true, + }, +}; + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + cards: YextField("Cards", { + type: "array", + defaultItemProps: defaultCard, + arrayFields: { + title: YextField("Title", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + description: YextField("Description", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + actionText: YextField("Action Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + actionHref: YextField("Action Href", { + type: "text", + }), + image: YextField("Image", { + type: "entityField", + filter: { types: ["type.image"] }, + }), + }, + }), + }, + }), +}; + +const YetiExploreCardsSlotComponent: PuckComponent< + YetiExploreCardsSlotProps +> = ({ data }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + return ( +
+ {data.cards.map((card, index) => { + const title = resolveComponentData( + card?.title, + i18n.language, + streamDocument + ); + const description = resolveComponentData( + card?.description, + i18n.language, + streamDocument + ); + const actionText = resolveComponentData( + card?.actionText, + i18n.language, + streamDocument + ); + const imageValue = resolveComponentData( + card?.image, + i18n.language, + streamDocument + ) as ImageValue | undefined; + const imageUrl = resolveImageUrl(imageValue); + + return ( +
+ {imageUrl ? ( + {typeof + ) : ( +
+ )} +
+ {title ? {title} : null} + {description ? ( + + {description} + + ) : null} + {actionText ? ( + + {actionText} + + ) : null} +
+
+ ); + })} +
+ ); +}; + +export const defaultYetiExploreCardsSlotProps: YetiExploreCardsSlotProps = { + data: { + cards: [ + { + title: toTranslatableString("Gift Wrapping"), + description: toTranslatableString( + "Complimentary wrapping paper and a YETI gift tag are available in store." + ), + actionText: toTranslatableString(""), + actionHref: "", + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/14c68cf7b51b5c21/original/230107_PLP_TV_Lifestyle_GiftWrap_Desktop-2x.jpg?auto=format,compress&h=750", + width: 1200, + height: 750, + }, + constantValueEnabled: true, + }, + }, + { + title: toTranslatableString("Visit Our Garage"), + description: toTranslatableString( + "Personalize your Tundra cooler only in select stores." + ), + actionText: toTranslatableString(""), + actionHref: "", + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/f7e93674cdfe9c6/original/230107_HP_TV_Product_1-4_Spotlight_Studio_GG_Desktop-2x.jpg?auto=format,compress&h=750", + width: 1200, + height: 750, + }, + constantValueEnabled: true, + }, + }, + { + title: toTranslatableString("Get Rewarded for Recycling"), + description: toTranslatableString( + "Receive $5 off a $25+ purchase for eligible Rambler products in store." + ), + actionText: toTranslatableString("See more"), + actionHref: "https://www.yeti.com/rambler-buy-back.html", + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/2d483e0edf43662d/original/230107_PLP_TV_Lifestyle_Recycle_Desktop-2x.jpg?auto=format,compress&h=750", + width: 1200, + height: 750, + }, + constantValueEnabled: true, + }, + }, + { + title: toTranslatableString("Live Music & Events"), + description: toTranslatableString( + "Check upcoming events, including live music and book tours." + ), + actionText: toTranslatableString("See more"), + actionHref: "https://www.facebook.com/Yeti/events", + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/5f18bc18f671f952/original/230107_PLP_TV_Lifestyle_Live_Events_Desktop-2x.jpg?auto=format,compress&h=750", + width: 1200, + height: 750, + }, + constantValueEnabled: true, + }, + }, + ], + }, +}; + +export const YetiExploreCardsSlot: ComponentConfig<{ + props: YetiExploreCardsSlotProps; +}> = { + label: "Yeti Explore Cards Slot", + fields, + defaultProps: defaultYetiExploreCardsSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiExploreCarouselSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiExploreCarouselSection.tsx new file mode 100644 index 0000000000..7b1b3b4674 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiExploreCarouselSection.tsx @@ -0,0 +1,122 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiSectionHeadingSlotProps } from "./YetiSectionHeadingSlot.tsx"; +import { defaultYetiExploreCardsSlotProps } from "./YetiExploreCardsSlot.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiExploreCarouselSectionProps { + styles: { + backgroundClassName: "bg-white" | "bg-neutral-100"; + showHeading: boolean; + showCards: boolean; + }; + slots: { + HeadingSlot: Slot; + CardsSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + backgroundClassName: YextField("Background", { + type: "select", + options: [ + { label: "White", value: "bg-white" }, + { label: "Soft Gray", value: "bg-neutral-100" }, + ], + }), + showHeading: YextField("Show Heading", { + type: "radio", + options: "SHOW_HIDE", + }), + showCards: YextField("Show Cards", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + HeadingSlot: { type: "slot" }, + CardsSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiExploreCarouselSectionComponent: PuckComponent< + YetiExploreCarouselSectionProps +> = ({ styles, slots }) => { + return ( + +
+ {styles.showHeading ? ( + + ) : null} + {styles.showCards ? ( + + ) : null} +
+
+ ); +}; + +export const YetiExploreCarouselSection: ComponentConfig<{ + props: YetiExploreCarouselSectionProps; +}> = { + label: "Yeti Explore Carousel Section", + fields, + defaultProps: { + styles: { + backgroundClassName: "bg-white", + showHeading: true, + showCards: true, + }, + slots: { + HeadingSlot: [ + { + type: "YetiSectionHeadingSlot", + props: { + ...defaultYetiSectionHeadingSlotProps, + data: { + text: toTranslatableString("EXPLORE OUR STORE"), + }, + styles: { + level: 3, + align: "left", + }, + }, + }, + ], + CardsSlot: [ + { + type: "YetiExploreCardsSlot", + props: defaultYetiExploreCardsSlotProps, + }, + ], + }, + liveVisibility: true, + }, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFaqListSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFaqListSlot.tsx new file mode 100644 index 0000000000..8f0b44f887 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFaqListSlot.tsx @@ -0,0 +1,140 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiParagraph } from "../atoms/YetiParagraph.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiFaqListSlotProps { + data: { + items: Array<{ + question: TranslatableString; + answer: TranslatableString; + }>; + }; +} + +const defaultFaqItem = { + question: toTranslatableString("Question"), + answer: toTranslatableString("Answer"), +}; + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + items: YextField("Items", { + type: "array", + defaultItemProps: defaultFaqItem, + arrayFields: { + question: YextField("Question", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + answer: YextField("Answer", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + }, + }), +}; + +const YetiFaqListSlotComponent: PuckComponent = ({ + data, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + return ( +
+ {data.items.map((item, index) => { + const question = resolveComponentData( + item?.question, + i18n.language, + streamDocument + ); + const answer = resolveComponentData( + item?.answer, + i18n.language, + streamDocument + ); + + if (!question && !answer) { + return null; + } + + return ( +
+ + {question} + + {answer ? ( + + {answer} + + ) : null} +
+ ); + })} +
+ ); +}; + +export const defaultYetiFaqListSlotProps: YetiFaqListSlotProps = { + data: { + items: [ + { + question: toTranslatableString("Are pets allowed in this store?"), + answer: toTranslatableString( + "Furry friends are welcome in our patio area, with fresh water available." + ), + }, + { + question: toTranslatableString("Where can I park?"), + answer: toTranslatableString( + "Parking is available in nearby garages and around surrounding streets." + ), + }, + { + question: toTranslatableString( + "Do you have drinkware with city-specific designs?" + ), + answer: toTranslatableString( + "Each store location offers drinkware artwork inspired by its city." + ), + }, + { + question: toTranslatableString("Is there curbside pickup?"), + answer: toTranslatableString( + "Yes. Shop online and associates can help load your order at pickup." + ), + }, + { + question: toTranslatableString( + "Can I host a private event at this store?" + ), + answer: toTranslatableString( + "Contact the local store team for details on private event options." + ), + }, + ], + }, +}; + +export const YetiFaqListSlot: ComponentConfig<{ props: YetiFaqListSlotProps }> = + { + label: "Yeti FAQ List Slot", + fields, + defaultProps: defaultYetiFaqListSlotProps, + render: (props) => , + }; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFaqSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFaqSection.tsx new file mode 100644 index 0000000000..75d0dc69fd --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFaqSection.tsx @@ -0,0 +1,121 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiSectionHeadingSlotProps } from "./YetiSectionHeadingSlot.tsx"; +import { defaultYetiFaqListSlotProps } from "./YetiFaqListSlot.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiFaqSectionProps { + styles: { + backgroundClassName: "bg-white" | "bg-neutral-100"; + showHeading: boolean; + showFaqList: boolean; + }; + slots: { + HeadingSlot: Slot; + FaqListSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + backgroundClassName: YextField("Background", { + type: "select", + options: [ + { label: "White", value: "bg-white" }, + { label: "Soft Gray", value: "bg-neutral-100" }, + ], + }), + showHeading: YextField("Show Heading", { + type: "radio", + options: "SHOW_HIDE", + }), + showFaqList: YextField("Show FAQ List", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + HeadingSlot: { type: "slot" }, + FaqListSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiFaqSectionComponent: PuckComponent = ({ + styles, + slots, +}) => { + return ( + +
+ {styles.showHeading ? ( + + ) : null} + {styles.showFaqList ? ( + + ) : null} +
+
+ ); +}; + +export const YetiFaqSection: ComponentConfig<{ props: YetiFaqSectionProps }> = { + label: "Yeti FAQ Section", + fields, + defaultProps: { + styles: { + backgroundClassName: "bg-white", + showHeading: true, + showFaqList: true, + }, + slots: { + HeadingSlot: [ + { + type: "YetiSectionHeadingSlot", + props: { + ...defaultYetiSectionHeadingSlotProps, + data: { + text: toTranslatableString("FAQ"), + }, + styles: { + level: 2, + align: "left", + }, + }, + }, + ], + FaqListSlot: [ + { + type: "YetiFaqListSlot", + props: defaultYetiFaqListSlotProps, + }, + ], + }, + liveVisibility: true, + }, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLayoutSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLayoutSlot.tsx new file mode 100644 index 0000000000..bb90b8d8f4 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLayoutSlot.tsx @@ -0,0 +1,201 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { defaultYetiFooterSignupSlotProps } from "./YetiFooterSignupSlot.tsx"; +import { + defaultYetiCompanyLinksColumnSlotProps, + defaultYetiComplianceLinksColumnSlotProps, + defaultYetiStoreLinksColumnSlotProps, + defaultYetiSupportLinksColumnSlotProps, +} from "./YetiFooterLinksColumnSlot.tsx"; +import { defaultYetiFooterSocialLinksSlotProps } from "./YetiFooterSocialLinksSlot.tsx"; +import { defaultYetiFooterLegalSlotProps } from "./YetiFooterLegalSlot.tsx"; + +export interface YetiFooterLayoutSlotProps { + styles: { + showSignup: boolean; + showSupportLinks: boolean; + showCompanyLinks: boolean; + showStoresLinks: boolean; + showComplianceLinks: boolean; + showSocial: boolean; + showLegal: boolean; + }; + slots: { + SignupSlot: Slot; + SupportLinksSlot: Slot; + CompanyLinksSlot: Slot; + StoresLinksSlot: Slot; + ComplianceLinksSlot: Slot; + SocialLinksSlot: Slot; + LegalSlot: Slot; + }; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + showSignup: YextField("Show Signup", { + type: "radio", + options: "SHOW_HIDE", + }), + showSupportLinks: YextField("Show Support Links", { + type: "radio", + options: "SHOW_HIDE", + }), + showCompanyLinks: YextField("Show Company Links", { + type: "radio", + options: "SHOW_HIDE", + }), + showStoresLinks: YextField("Show Stores Links", { + type: "radio", + options: "SHOW_HIDE", + }), + showComplianceLinks: YextField("Show Compliance Links", { + type: "radio", + options: "SHOW_HIDE", + }), + showSocial: YextField("Show Social", { + type: "radio", + options: "SHOW_HIDE", + }), + showLegal: YextField("Show Legal", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + SignupSlot: { type: "slot" }, + SupportLinksSlot: { type: "slot" }, + CompanyLinksSlot: { type: "slot" }, + StoresLinksSlot: { type: "slot" }, + ComplianceLinksSlot: { type: "slot" }, + SocialLinksSlot: { type: "slot" }, + LegalSlot: { type: "slot" }, + }, + visible: false, + }, +}; + +const YetiFooterLayoutSlotComponent: PuckComponent< + YetiFooterLayoutSlotProps +> = ({ styles, slots }) => { + return ( +
+ {styles.showSignup ? ( +
+ +
+ ) : null} + {(styles.showSupportLinks || + styles.showCompanyLinks || + styles.showStoresLinks || + styles.showComplianceLinks) && ( +
+ {styles.showSupportLinks ? ( +
+ +
+ ) : null} + {styles.showCompanyLinks ? ( +
+ +
+ ) : null} + {styles.showStoresLinks ? ( +
+ +
+ ) : null} + {styles.showComplianceLinks ? ( +
+ +
+ ) : null} +
+ )} + {styles.showSocial ? ( +
+ +
+ ) : null} + {styles.showLegal ? ( +
+ +
+ ) : null} +
+ ); +}; + +export const defaultYetiFooterLayoutSlotProps: YetiFooterLayoutSlotProps = { + styles: { + showSignup: true, + showSupportLinks: true, + showCompanyLinks: true, + showStoresLinks: true, + showComplianceLinks: true, + showSocial: true, + showLegal: true, + }, + slots: { + SignupSlot: [ + { + type: "YetiFooterSignupSlot", + props: defaultYetiFooterSignupSlotProps, + }, + ], + SupportLinksSlot: [ + { + type: "YetiFooterLinksColumnSlot", + props: defaultYetiSupportLinksColumnSlotProps, + }, + ], + CompanyLinksSlot: [ + { + type: "YetiFooterLinksColumnSlot", + props: defaultYetiCompanyLinksColumnSlotProps, + }, + ], + StoresLinksSlot: [ + { + type: "YetiFooterLinksColumnSlot", + props: defaultYetiStoreLinksColumnSlotProps, + }, + ], + ComplianceLinksSlot: [ + { + type: "YetiFooterLinksColumnSlot", + props: defaultYetiComplianceLinksColumnSlotProps, + }, + ], + SocialLinksSlot: [ + { + type: "YetiFooterSocialLinksSlot", + props: defaultYetiFooterSocialLinksSlotProps, + }, + ], + LegalSlot: [ + { + type: "YetiFooterLegalSlot", + props: defaultYetiFooterLegalSlotProps, + }, + ], + }, +}; + +export const YetiFooterLayoutSlot: ComponentConfig<{ + props: YetiFooterLayoutSlotProps; +}> = { + label: "Yeti Footer Layout Slot", + fields, + defaultProps: defaultYetiFooterLayoutSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLegalSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLegalSlot.tsx new file mode 100644 index 0000000000..2cefe61e22 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLegalSlot.tsx @@ -0,0 +1,118 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiFooterLegalSlotProps { + data: { + copyrightText: TranslatableString; + legalLinks: Array<{ + label: TranslatableString; + href: string; + }>; + }; +} + +const defaultLegalLink = { + label: toTranslatableString("Legal Link"), + href: "#", +}; + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + copyrightText: YextField("Copyright Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + legalLinks: YextField("Legal Links", { + type: "array", + defaultItemProps: defaultLegalLink, + arrayFields: { + label: YextField("Label", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + href: YextField("Href", { + type: "text", + }), + }, + }), + }, + }), +}; + +const YetiFooterLegalSlotComponent: PuckComponent = ({ + data, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const copyrightText = resolveComponentData( + data.copyrightText, + i18n.language, + streamDocument + ); + + return ( +
+ {copyrightText ?

{copyrightText}

: null} +
+ {data.legalLinks.map((link, index) => { + const label = resolveComponentData( + link?.label, + i18n.language, + streamDocument + ); + if (!label) { + return null; + } + + return ( + + {label} + + ); + })} +
+
+ ); +}; + +export const defaultYetiFooterLegalSlotProps: YetiFooterLegalSlotProps = { + data: { + copyrightText: toTranslatableString( + "© YETI COOLERS, LLC. All rights reserved." + ), + legalLinks: [ + { + label: toTranslatableString("Privacy Policy"), + href: "https://www.yeti.com/privacy-policy.html", + }, + { + label: toTranslatableString("Terms & Conditions"), + href: "https://www.yeti.com/terms-conditions.html", + }, + ], + }, +}; + +export const YetiFooterLegalSlot: ComponentConfig<{ + props: YetiFooterLegalSlotProps; +}> = { + label: "Yeti Footer Legal Slot", + fields, + defaultProps: defaultYetiFooterLegalSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLinksColumnSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLinksColumnSlot.tsx new file mode 100644 index 0000000000..43da29b28e --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterLinksColumnSlot.tsx @@ -0,0 +1,227 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiFooterLinksColumnSlotProps { + data: { + heading: TranslatableString; + links: Array<{ + label: TranslatableString; + href: string; + openInNewTab: boolean; + }>; + }; +} + +const defaultLink = { + label: toTranslatableString("Link"), + href: "#", + openInNewTab: false, +}; + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + heading: YextField("Heading", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + links: YextField("Links", { + type: "array", + defaultItemProps: defaultLink, + arrayFields: { + label: YextField("Label", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + href: YextField("Href", { type: "text" }), + openInNewTab: YextField("Open in New Tab", { + type: "radio", + options: [ + { label: "No", value: false }, + { label: "Yes", value: true }, + ], + }), + }, + }), + }, + }), +}; + +const YetiFooterLinksColumnSlotComponent: PuckComponent< + YetiFooterLinksColumnSlotProps +> = ({ data }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const heading = resolveComponentData( + data.heading, + i18n.language, + streamDocument + ); + + return ( +
+ {heading ? {heading} : null} +
    + {data.links.map((link, index) => { + const label = resolveComponentData( + link?.label, + i18n.language, + streamDocument + ); + if (!label) { + return null; + } + + return ( +
  • + + {label} + +
  • + ); + })} +
+
+ ); +}; + +export const defaultYetiSupportLinksColumnSlotProps: YetiFooterLinksColumnSlotProps = + { + data: { + heading: toTranslatableString("Customer Support"), + links: [ + { + label: toTranslatableString("Help"), + href: "https://www.yeti.com/help-guide.html", + openInNewTab: false, + }, + { + label: toTranslatableString("FAQ"), + href: "https://www.yeti.com/faq.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Contact"), + href: "https://www.yeti.com/contact-us.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Shipping"), + href: "https://www.yeti.com/shipping-and-returns.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Returns"), + href: "https://www.yeti.com/returns", + openInNewTab: false, + }, + ], + }, + }; + +export const defaultYetiCompanyLinksColumnSlotProps: YetiFooterLinksColumnSlotProps = + { + data: { + heading: toTranslatableString("Company"), + links: [ + { + label: toTranslatableString("About Us"), + href: "https://www.yeti.com/stories/our-story.html", + openInNewTab: false, + }, + { + label: toTranslatableString("News"), + href: "https://www.yeti.com/news.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Careers"), + href: "https://www.yeti.com/yeti-careers.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Corporate Sales"), + href: "https://www.yeti.com/corporate-sales.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Investor Relations"), + href: "https://investors.yeti.com/overview/default.aspx", + openInNewTab: true, + }, + ], + }, + }; + +export const defaultYetiStoreLinksColumnSlotProps: YetiFooterLinksColumnSlotProps = + { + data: { + heading: toTranslatableString("Stores"), + links: [ + { + label: toTranslatableString("See All Stores"), + href: "https://www.yeti.com/yeti-store-locations.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Dealer Locator"), + href: "https://www.yeti.com/find-a-store", + openInNewTab: false, + }, + ], + }, + }; + +export const defaultYetiComplianceLinksColumnSlotProps: YetiFooterLinksColumnSlotProps = + { + data: { + heading: toTranslatableString("Privacy & Compliance"), + links: [ + { + label: toTranslatableString("Privacy Policy"), + href: "https://www.yeti.com/privacy-policy.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Terms & Conditions"), + href: "https://www.yeti.com/terms-conditions.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Cookie Policy"), + href: "https://www.yeti.com/cookie-policy.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Report a Vulnerability"), + href: "https://www.yeti.com/report-vulnerability.html", + openInNewTab: false, + }, + ], + }, + }; + +export const YetiFooterLinksColumnSlot: ComponentConfig<{ + props: YetiFooterLinksColumnSlotProps; +}> = { + label: "Yeti Footer Links Column Slot", + fields, + defaultProps: defaultYetiSupportLinksColumnSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSection.tsx new file mode 100644 index 0000000000..765733d048 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSection.tsx @@ -0,0 +1,121 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiFooterLayoutSlotProps } from "./YetiFooterLayoutSlot.tsx"; + +export interface YetiFooterSectionProps { + styles: { + backgroundClassName: "bg-neutral-100" | "bg-white" | "bg-[#0F3658]"; + textClassName: "text-neutral-900" | "text-white"; + dividerClassName: "border-black/15" | "border-white/30"; + showTopDivider: boolean; + showFooterLayout: boolean; + }; + slots: { + FooterLayoutSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + backgroundClassName: YextField("Background", { + type: "select", + options: [ + { label: "Light Gray", value: "bg-neutral-100" }, + { label: "White", value: "bg-white" }, + { label: "Yeti Blue", value: "bg-[#0F3658]" }, + ], + }), + textClassName: YextField("Text Color", { + type: "select", + options: [ + { label: "Dark", value: "text-neutral-900" }, + { label: "Light", value: "text-white" }, + ], + }), + dividerClassName: YextField("Divider Color", { + type: "select", + options: [ + { label: "Dark", value: "border-black/15" }, + { label: "Light", value: "border-white/30" }, + ], + }), + showTopDivider: YextField("Show Top Divider", { + type: "radio", + options: "SHOW_HIDE", + }), + showFooterLayout: YextField("Show Footer Layout", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + FooterLayoutSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiFooterSectionComponent: PuckComponent = ({ + styles, + slots, +}) => { + return ( + + {styles.showFooterLayout ? ( + + ) : null} + + ); +}; + +export const YetiFooterSection: ComponentConfig<{ + props: YetiFooterSectionProps; +}> = { + label: "Yeti Footer Section", + fields, + defaultProps: { + styles: { + backgroundClassName: "bg-[#0F3658]", + textClassName: "text-white", + dividerClassName: "border-white/30", + showTopDivider: true, + showFooterLayout: true, + }, + slots: { + FooterLayoutSlot: [ + { + type: "YetiFooterLayoutSlot", + props: defaultYetiFooterLayoutSlotProps, + }, + ], + }, + liveVisibility: true, + }, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSignupSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSignupSlot.tsx new file mode 100644 index 0000000000..4ebabcb57e --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSignupSlot.tsx @@ -0,0 +1,141 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { YetiParagraph } from "../atoms/YetiParagraph.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiFooterSignupSlotProps { + data: { + heading: TranslatableString; + body: TranslatableString; + inputPlaceholder: TranslatableString; + actionText: TranslatableString; + actionHref: string; + disclaimer: TranslatableString; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + heading: YextField("Heading", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + body: YextField("Body", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + inputPlaceholder: YextField("Input Placeholder", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + actionText: YextField("Action Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + actionHref: YextField("Action Href", { type: "text" }), + disclaimer: YextField("Disclaimer", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), +}; + +const YetiFooterSignupSlotComponent: PuckComponent< + YetiFooterSignupSlotProps +> = ({ data }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const heading = resolveComponentData( + data.heading, + i18n.language, + streamDocument + ); + const body = resolveComponentData(data.body, i18n.language, streamDocument); + const inputPlaceholder = resolveComponentData( + data.inputPlaceholder, + i18n.language, + streamDocument + ); + const actionText = resolveComponentData( + data.actionText, + i18n.language, + streamDocument + ); + const disclaimer = resolveComponentData( + data.disclaimer, + i18n.language, + streamDocument + ); + + return ( +
+ {heading ? {heading} : null} + {body ? ( + + {body} + + ) : null} +
+ + {actionText ? ( + + {actionText} + + ) : null} +
+ {disclaimer ? ( + + {disclaimer} + + ) : null} +
+ ); +}; + +export const defaultYetiFooterSignupSlotProps: YetiFooterSignupSlotProps = { + data: { + heading: toTranslatableString("Sign Me Up"), + body: toTranslatableString( + "Be the first to know about new products, films, and events." + ), + inputPlaceholder: toTranslatableString("Enter Your Email"), + actionText: toTranslatableString("Join"), + actionHref: "https://www.yeti.com/", + disclaimer: toTranslatableString( + "By entering your email address you agree to receive marketing messages from YETI." + ), + }, +}; + +export const YetiFooterSignupSlot: ComponentConfig<{ + props: YetiFooterSignupSlotProps; +}> = { + label: "Yeti Footer Signup Slot", + fields, + defaultProps: defaultYetiFooterSignupSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSocialLinksSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSocialLinksSlot.tsx new file mode 100644 index 0000000000..1158cc4462 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiFooterSocialLinksSlot.tsx @@ -0,0 +1,172 @@ +// @ts-nocheck +import * as React from "react"; +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { + FaFacebook, + FaInstagram, + FaLinkedinIn, + FaYoutube, + FaPinterest, + FaTiktok, +} from "react-icons/fa"; +import { FaXTwitter } from "react-icons/fa6"; + +const validPatterns = { + xLink: /^(https?:\/\/)?(www\.)?(x\.com|twitter\.com)\/.+/i, + facebookLink: /^(https?:\/\/)?(www\.)?facebook\.com\/.+/i, + instagramLink: /^(https?:\/\/)?(www\.)?instagram\.com\/.+/i, + linkedInLink: /^(https?:\/\/)?(www\.)?linkedin\.com\/.+/i, + pinterestLink: /^(https?:\/\/)?(www\.)?pinterest\.com\/.+/i, + tiktokLink: /^(https?:\/\/)?(www\.)?tiktok\.com\/.+/i, + youtubeLink: /^(https?:\/\/)?(www\.)?(youtube\.com|youtu\.be)\/.+/i, +}; + +export interface YetiFooterSocialLinksSlotProps { + data: { + xLink: string; + facebookLink: string; + instagramLink: string; + linkedInLink: string; + pinterestLink: string; + tiktokLink: string; + youtubeLink: string; + }; + styles: { + mobileAlignment: "left" | "center"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + xLink: YextField("X Link", { type: "text" }), + facebookLink: YextField("Facebook Link", { type: "text" }), + instagramLink: YextField("Instagram Link", { type: "text" }), + linkedInLink: YextField("LinkedIn Link", { type: "text" }), + pinterestLink: YextField("Pinterest Link", { type: "text" }), + tiktokLink: YextField("TikTok Link", { type: "text" }), + youtubeLink: YextField("YouTube Link", { type: "text" }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + mobileAlignment: YextField("Mobile Alignment", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + ], + }), + }, + }), +}; + +type SocialLink = { + url: string; + icon: React.ComponentType<{ className?: string }>; + pattern: RegExp; + label: string; +}; + +const YetiFooterSocialLinksSlotComponent: PuckComponent< + YetiFooterSocialLinksSlotProps +> = ({ data, styles, puck }) => { + const links: SocialLink[] = [ + { + url: data.xLink, + icon: FaXTwitter, + pattern: validPatterns.xLink, + label: "X", + }, + { + url: data.facebookLink, + icon: FaFacebook, + pattern: validPatterns.facebookLink, + label: "Facebook", + }, + { + url: data.instagramLink, + icon: FaInstagram, + pattern: validPatterns.instagramLink, + label: "Instagram", + }, + { + url: data.linkedInLink, + icon: FaLinkedinIn, + pattern: validPatterns.linkedInLink, + label: "LinkedIn", + }, + { + url: data.pinterestLink, + icon: FaPinterest, + pattern: validPatterns.pinterestLink, + label: "Pinterest", + }, + { + url: data.tiktokLink, + icon: FaTiktok, + pattern: validPatterns.tiktokLink, + label: "TikTok", + }, + { + url: data.youtubeLink, + icon: FaYoutube, + pattern: validPatterns.youtubeLink, + label: "YouTube", + }, + ].filter((link) => link.url && link.pattern.test(link.url)); + + if (!links.length) { + return puck.isEditing ?
: null; + } + + return ( +
+ {links.map((link) => { + const Icon = link.icon; + return ( + + + + ); + })} +
+ ); +}; + +export const defaultYetiFooterSocialLinksSlotProps: YetiFooterSocialLinksSlotProps = + { + data: { + xLink: "", + facebookLink: "https://www.facebook.com/Yeti/", + instagramLink: "https://www.instagram.com/yeti", + linkedInLink: "", + pinterestLink: "", + tiktokLink: "https://www.tiktok.com/@yeti", + youtubeLink: "https://www.youtube.com/channel/UCAZ5PoEUL2_clEdDBrFF-aQ", + }, + styles: { + mobileAlignment: "left", + }, + }; + +export const YetiFooterSocialLinksSlot: ComponentConfig<{ + props: YetiFooterSocialLinksSlotProps; +}> = { + label: "Yeti Footer Social Links Slot", + fields, + defaultProps: defaultYetiFooterSocialLinksSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiHeaderLayoutSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiHeaderLayoutSlot.tsx new file mode 100644 index 0000000000..4fe278b539 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiHeaderLayoutSlot.tsx @@ -0,0 +1,169 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; +import { + defaultYetiPrimaryNavLinksSlotProps, + defaultYetiUtilityNavLinksSlotProps, +} from "./YetiNavLinksSlot.tsx"; +import { defaultYetiBrandSlotProps } from "./YetiBrandSlot.tsx"; +import { defaultYetiPrimaryActionSlotProps } from "./YetiPrimaryActionSlot.tsx"; + +export interface YetiHeaderLayoutSlotProps { + data: { + announcementText: TranslatableString; + }; + styles: { + showAnnouncement: boolean; + showBrand: boolean; + showPrimaryLinks: boolean; + showUtilityLinks: boolean; + showPrimaryAction: boolean; + }; + slots: { + BrandSlot: Slot; + PrimaryLinksSlot: Slot; + UtilityLinksSlot: Slot; + PrimaryActionSlot: Slot; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + announcementText: YextField("Announcement Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + showAnnouncement: YextField("Show Announcement", { + type: "radio", + options: "SHOW_HIDE", + }), + showBrand: YextField("Show Brand", { + type: "radio", + options: "SHOW_HIDE", + }), + showPrimaryLinks: YextField("Show Primary Links", { + type: "radio", + options: "SHOW_HIDE", + }), + showUtilityLinks: YextField("Show Utility Links", { + type: "radio", + options: "SHOW_HIDE", + }), + showPrimaryAction: YextField("Show Primary Action", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + BrandSlot: { type: "slot" }, + PrimaryLinksSlot: { type: "slot" }, + UtilityLinksSlot: { type: "slot" }, + PrimaryActionSlot: { type: "slot" }, + }, + visible: false, + }, +}; + +const YetiHeaderLayoutSlotComponent: PuckComponent< + YetiHeaderLayoutSlotProps +> = ({ data, styles, slots }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + const announcementText = resolveComponentData( + data.announcementText, + i18n.language, + streamDocument + ); + + return ( +
+ {styles.showAnnouncement && announcementText ? ( +
+ {announcementText} +
+ ) : null} +
+ {styles.showBrand ? ( +
+ +
+ ) : null} + {styles.showPrimaryLinks ? ( +
+ +
+ ) : null} +
+ {(styles.showUtilityLinks || styles.showPrimaryAction) && ( +
+
+ {styles.showUtilityLinks ? ( + + ) : null} +
+ {styles.showPrimaryAction ? ( +
+ +
+ ) : null} +
+ )} +
+ ); +}; + +export const defaultYetiHeaderLayoutSlotProps: YetiHeaderLayoutSlotProps = { + data: { + announcementText: toTranslatableString( + "Free customization in select YETI stores" + ), + }, + styles: { + showAnnouncement: true, + showBrand: true, + showPrimaryLinks: true, + showUtilityLinks: true, + showPrimaryAction: true, + }, + slots: { + BrandSlot: [{ type: "YetiBrandSlot", props: defaultYetiBrandSlotProps }], + PrimaryLinksSlot: [ + { type: "YetiNavLinksSlot", props: defaultYetiPrimaryNavLinksSlotProps }, + ], + UtilityLinksSlot: [ + { type: "YetiNavLinksSlot", props: defaultYetiUtilityNavLinksSlotProps }, + ], + PrimaryActionSlot: [ + { + type: "YetiPrimaryActionSlot", + props: defaultYetiPrimaryActionSlotProps, + }, + ], + }, +}; + +export const YetiHeaderLayoutSlot: ComponentConfig<{ + props: YetiHeaderLayoutSlotProps; +}> = { + label: "Yeti Header Layout Slot", + fields, + defaultProps: defaultYetiHeaderLayoutSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiHeaderSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiHeaderSection.tsx new file mode 100644 index 0000000000..bf9ae4a819 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiHeaderSection.tsx @@ -0,0 +1,123 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiHeaderLayoutSlotProps } from "./YetiHeaderLayoutSlot.tsx"; + +export interface YetiHeaderSectionProps { + styles: { + backgroundClassName: "bg-neutral-100" | "bg-white" | "bg-[#0F3658]"; + textClassName: "text-neutral-900" | "text-white"; + dividerClassName: "border-black/15" | "border-white/30"; + showBottomDivider: boolean; + showHeaderLayout: boolean; + }; + slots: { + HeaderLayoutSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + backgroundClassName: YextField("Background", { + type: "select", + options: [ + { label: "Light Gray", value: "bg-neutral-100" }, + { label: "White", value: "bg-white" }, + { label: "Yeti Blue", value: "bg-[#0F3658]" }, + ], + }), + textClassName: YextField("Text Color", { + type: "select", + options: [ + { label: "Dark", value: "text-neutral-900" }, + { label: "Light", value: "text-white" }, + ], + }), + dividerClassName: YextField("Divider Color", { + type: "select", + options: [ + { label: "Dark", value: "border-black/15" }, + { label: "Light", value: "border-white/30" }, + ], + }), + showBottomDivider: YextField("Show Bottom Divider", { + type: "radio", + options: "SHOW_HIDE", + }), + showHeaderLayout: YextField("Show Header Layout", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + HeaderLayoutSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiHeaderSectionComponent: PuckComponent = ({ + styles, + slots, +}) => { + return ( + + {styles.showHeaderLayout ? ( + + ) : null} + + ); +}; + +export const YetiHeaderSection: ComponentConfig<{ + props: YetiHeaderSectionProps; +}> = { + label: "Yeti Header Section", + fields, + defaultProps: { + styles: { + backgroundClassName: "bg-[#0F3658]", + textClassName: "text-white", + dividerClassName: "border-white/30", + showBottomDivider: true, + showHeaderLayout: true, + }, + slots: { + HeaderLayoutSlot: [ + { + type: "YetiHeaderLayoutSlot", + props: defaultYetiHeaderLayoutSlotProps, + }, + ], + }, + liveVisibility: true, + }, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiHeroHeadingSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiHeroHeadingSlot.tsx new file mode 100644 index 0000000000..e3ef4b7a9a --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiHeroHeadingSlot.tsx @@ -0,0 +1,112 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + EntityField, + TranslatableString, + YextEntityField, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiHeroHeadingSlotProps { + data: { + heading: YextEntityField; + }; + styles: { + level: 1 | 2 | 3 | 4; + align: "left" | "center"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + heading: YextField("Heading", { + type: "entityField", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + level: YextField("Level", { + type: "select", + options: [ + { label: "H1", value: 1 }, + { label: "H2", value: 2 }, + { label: "H3", value: 3 }, + { label: "H4", value: 4 }, + ], + }), + align: YextField("Align", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + ], + }), + }, + }), +}; + +const YetiHeroHeadingSlotComponent: PuckComponent = ({ + data, + styles, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const heading = resolveComponentData( + data.heading, + i18n.language, + streamDocument + ); + + if (!heading) { + return null; + } + + return ( + + + {heading} + + + ); +}; + +export const defaultYetiHeroHeadingSlotProps: YetiHeroHeadingSlotProps = { + data: { + heading: { + field: "name", + constantValue: toTranslatableString("YETI Store"), + constantValueEnabled: false, + }, + }, + styles: { + level: 1, + align: "left", + }, +}; + +export const YetiHeroHeadingSlot: ComponentConfig<{ + props: YetiHeroHeadingSlotProps; +}> = { + label: "Yeti Hero Heading Slot", + fields, + defaultProps: defaultYetiHeroHeadingSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiHeroImageSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiHeroImageSlot.tsx new file mode 100644 index 0000000000..3435a008ff --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiHeroImageSlot.tsx @@ -0,0 +1,136 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextEntityField, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +type ImageValue = { + url?: string; + width?: number; + height?: number; + image?: { + url?: string; + width?: number; + height?: number; + }; +}; + +const resolveImageUrl = (value: ImageValue | undefined): string => { + if (!value) { + return ""; + } + if (value.url) { + return value.url; + } + if (value.image?.url) { + return value.image.url; + } + return ""; +}; + +export interface YetiHeroImageSlotProps { + data: { + image: YextEntityField; + altText: TranslatableString; + }; + styles: { + minHeight: number; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + image: YextField("Image", { + type: "entityField", + filter: { types: ["type.image"] }, + }), + altText: YextField("Alt Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + minHeight: YextField("Min Height", { + type: "number", + min: 200, + }), + }, + }), +}; + +const YetiHeroImageSlotComponent: PuckComponent = ({ + data, + styles, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const image = resolveComponentData( + data.image, + i18n.language, + streamDocument + ) as ImageValue | undefined; + const altText = resolveComponentData( + data.altText, + i18n.language, + streamDocument + ); + const imageUrl = resolveImageUrl(image); + + if (!imageUrl) { + return ( +
+ ); + } + + return ( + {typeof + ); +}; + +export const defaultYetiHeroImageSlotProps: YetiHeroImageSlotProps = { + data: { + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/7f27a5902316a8a9/original/230107_PLP_BMD_3-0_Paragraph_Lifestyle_Denver_Desktop-2x.jpg?auto=format,compress", + width: 1920, + height: 1080, + }, + constantValueEnabled: true, + }, + altText: toTranslatableString("YETI store hero"), + }, + styles: { + minHeight: 500, + }, +}; + +export const YetiHeroImageSlot: ComponentConfig<{ + props: YetiHeroImageSlotProps; +}> = { + label: "Yeti Hero Image Slot", + fields, + defaultProps: defaultYetiHeroImageSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiHoursSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiHoursSlot.tsx new file mode 100644 index 0000000000..1dca5e3c56 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiHoursSlot.tsx @@ -0,0 +1,142 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { HoursType } from "@yext/pages-components"; +import { + EntityField, + TranslatableString, + YextEntityField, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { YetiHoursTable } from "../atoms/YetiHoursTable.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiHoursSlotProps { + data: { + heading: TranslatableString; + hours: YextEntityField; + }; + styles: { + collapseDays: boolean; + startOfWeek: + | "monday" + | "tuesday" + | "wednesday" + | "thursday" + | "friday" + | "saturday" + | "sunday" + | "today"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + heading: YextField("Heading", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + hours: YextField("Hours", { + type: "entityField", + filter: { types: ["type.hours"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + collapseDays: YextField("Collapse Days", { + type: "radio", + options: [ + { label: "No", value: false }, + { label: "Yes", value: true }, + ], + }), + startOfWeek: YextField("Start of Week", { + type: "select", + options: [ + { label: "Today", value: "today" }, + { label: "Monday", value: "monday" }, + { label: "Tuesday", value: "tuesday" }, + { label: "Wednesday", value: "wednesday" }, + { label: "Thursday", value: "thursday" }, + { label: "Friday", value: "friday" }, + { label: "Saturday", value: "saturday" }, + { label: "Sunday", value: "sunday" }, + ], + }), + }, + }), +}; + +const YetiHoursSlotComponent: PuckComponent = ({ + data, + styles, + puck, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const heading = resolveComponentData( + data.heading, + i18n.language, + streamDocument + ); + const hours = resolveComponentData(data.hours, i18n.language, streamDocument); + + if (!hours) { + return puck.isEditing ?
: null; + } + + return ( +
+ {heading ? ( + + {heading} + + ) : null} + + + +
+ ); +}; + +export const defaultYetiHoursSlotProps: YetiHoursSlotProps = { + data: { + heading: toTranslatableString("Store Hours"), + hours: { + field: "hours", + constantValue: {}, + constantValueEnabled: false, + }, + }, + styles: { + collapseDays: false, + startOfWeek: "today", + }, +}; + +export const YetiHoursSlot: ComponentConfig<{ props: YetiHoursSlotProps }> = { + label: "Yeti Hours Slot", + fields, + defaultProps: defaultYetiHoursSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiLocationDetailsSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiLocationDetailsSlot.tsx new file mode 100644 index 0000000000..3eec382e48 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiLocationDetailsSlot.tsx @@ -0,0 +1,209 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + Address as RenderAddress, + AddressType, + getDirections, +} from "@yext/pages-components"; +import { + EntityField, + TranslatableString, + YextEntityField, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiLocationDetailsSlotProps { + data: { + title: TranslatableString; + address: YextEntityField; + phone: YextEntityField; + primaryActionText: TranslatableString; + secondaryActionText: TranslatableString; + }; + styles: { + showPrimaryAction: boolean; + showSecondaryAction: boolean; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + title: YextField("Title", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + address: YextField("Address", { + type: "entityField", + filter: { types: ["type.address"] }, + }), + phone: YextField("Phone", { + type: "entityField", + filter: { types: ["type.phone"] }, + }), + primaryActionText: YextField("Primary Action Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + secondaryActionText: YextField("Secondary Action Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + showPrimaryAction: YextField("Show Primary Action", { + type: "radio", + options: "SHOW_HIDE", + }), + showSecondaryAction: YextField("Show Secondary Action", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), +}; + +const YetiLocationDetailsSlotComponent: PuckComponent< + YetiLocationDetailsSlotProps +> = ({ data, styles, puck }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const title = resolveComponentData(data.title, i18n.language, streamDocument); + const address = resolveComponentData( + data.address, + i18n.language, + streamDocument + ) as AddressType | undefined; + const phone = resolveComponentData(data.phone, i18n.language, streamDocument); + const primaryActionText = resolveComponentData( + data.primaryActionText, + i18n.language, + streamDocument + ); + const secondaryActionText = resolveComponentData( + data.secondaryActionText, + i18n.language, + streamDocument + ); + + const hasAddress = Boolean( + address?.line1 || + address?.line2 || + address?.city || + address?.region || + address?.postalCode + ); + const directionsLink = hasAddress + ? getDirections(address, undefined, undefined, { provider: "google" }) + : undefined; + + if (!hasAddress && !phone) { + return puck.isEditing ?
: null; + } + + return ( +
+ {title ? ( + + {title} + + ) : null} + {hasAddress ? ( + +
+ +
+
+ ) : null} + {phone ? ( + + + {phone} + + + ) : null} +
+ {styles.showPrimaryAction && primaryActionText && directionsLink ? ( + + {primaryActionText} + + ) : null} + {styles.showSecondaryAction && secondaryActionText && phone ? ( + + {secondaryActionText} + + ) : null} +
+
+ ); +}; + +export const defaultYetiLocationDetailsSlotProps: YetiLocationDetailsSlotProps = + { + data: { + title: toTranslatableString("Location"), + address: { + field: "address", + constantValue: { + line1: "", + city: "", + region: "", + postalCode: "", + countryCode: "US", + }, + constantValueEnabled: false, + }, + phone: { + field: "mainPhone", + constantValue: "", + constantValueEnabled: false, + }, + primaryActionText: toTranslatableString("Get Directions"), + secondaryActionText: toTranslatableString("Call Us"), + }, + styles: { + showPrimaryAction: true, + showSecondaryAction: true, + }, + }; + +export const YetiLocationDetailsSlot: ComponentConfig<{ + props: YetiLocationDetailsSlotProps; +}> = { + label: "Yeti Location Details Slot", + fields, + defaultProps: defaultYetiLocationDetailsSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiLocationHeroSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiLocationHeroSection.tsx new file mode 100644 index 0000000000..cea014c146 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiLocationHeroSection.tsx @@ -0,0 +1,175 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiHeroHeadingSlotProps } from "./YetiHeroHeadingSlot.tsx"; +import { defaultYetiHeroImageSlotProps } from "./YetiHeroImageSlot.tsx"; +import { defaultYetiBodyCopySlotProps } from "./YetiBodyCopySlot.tsx"; +import { defaultYetiPrimaryActionSlotProps } from "./YetiPrimaryActionSlot.tsx"; + +export interface YetiLocationHeroSectionProps { + styles: { + contentAlign: "left" | "center"; + showImage: boolean; + showHeading: boolean; + showBody: boolean; + showAction: boolean; + }; + slots: { + HeroImageSlot: Slot; + HeroHeadingSlot: Slot; + HeroBodySlot: Slot; + HeroActionSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + contentAlign: YextField("Content Align", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + ], + }), + showImage: YextField("Show Image", { + type: "radio", + options: "SHOW_HIDE", + }), + showHeading: YextField("Show Heading", { + type: "radio", + options: "SHOW_HIDE", + }), + showBody: YextField("Show Body", { + type: "radio", + options: "SHOW_HIDE", + }), + showAction: YextField("Show Action", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + HeroImageSlot: { type: "slot" }, + HeroHeadingSlot: { type: "slot" }, + HeroBodySlot: { type: "slot" }, + HeroActionSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiLocationHeroSectionComponent: PuckComponent< + YetiLocationHeroSectionProps +> = ({ styles, slots }) => { + const isCentered = styles.contentAlign === "center"; + + return ( + +
+ {styles.showImage ? ( + + ) : ( +
+ )} +
+
+
+ {styles.showHeading ? ( +
+ +
+ ) : null} + {styles.showBody ? ( +
+ +
+ ) : null} + {styles.showAction ? ( +
+ +
+ ) : null} +
+
+
+
+ ); +}; + +const heroBodyDefaults = { + ...defaultYetiBodyCopySlotProps, + data: { + ...defaultYetiBodyCopySlotProps.data, + copy: { + en: "Experience YETI gear, drinkware, and customizations in-store.", + hasLocalizedValue: "true", + }, + }, +}; + +export const YetiLocationHeroSection: ComponentConfig<{ + props: YetiLocationHeroSectionProps; +}> = { + label: "Yeti Location Hero Section", + fields, + defaultProps: { + styles: { + contentAlign: "left", + showImage: true, + showHeading: true, + showBody: false, + showAction: true, + }, + slots: { + HeroImageSlot: [ + { + type: "YetiHeroImageSlot", + props: defaultYetiHeroImageSlotProps, + }, + ], + HeroHeadingSlot: [ + { + type: "YetiHeroHeadingSlot", + props: defaultYetiHeroHeadingSlotProps, + }, + ], + HeroBodySlot: [ + { + type: "YetiBodyCopySlot", + props: heroBodyDefaults, + }, + ], + HeroActionSlot: [ + { + type: "YetiPrimaryActionSlot", + props: defaultYetiPrimaryActionSlotProps, + }, + ], + }, + liveVisibility: true, + }, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiMapSurfaceSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiMapSurfaceSlot.tsx new file mode 100644 index 0000000000..4374ea288e --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiMapSurfaceSlot.tsx @@ -0,0 +1,174 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { AddressType, Coordinate, getDirections } from "@yext/pages-components"; +import { + EntityField, + TranslatableString, + YextEntityField, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiMapSurfaceSlotProps { + data: { + apiKey: string; + coordinate: YextEntityField; + address: YextEntityField; + directionsText: TranslatableString; + }; + styles: { + mapStyle: string; + showDirectionsAction: boolean; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + apiKey: YextField("API Key", { type: "text" }), + coordinate: YextField("Coordinate", { + type: "entityField", + filter: { types: ["type.coordinate"] }, + }), + address: YextField("Address", { + type: "entityField", + filter: { types: ["type.address"] }, + }), + directionsText: YextField("Directions Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + mapStyle: YextField("Map Style", { + type: "select", + options: [ + { label: "Default", value: "streets-v12" }, + { label: "Satellite", value: "satellite-streets-v12" }, + { label: "Light", value: "light-v11" }, + { label: "Dark", value: "dark-v11" }, + ], + }), + showDirectionsAction: YextField("Show Directions Action", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), +}; + +const YetiMapSurfaceSlotComponent: PuckComponent = ({ + data, + styles, + puck, +}) => { + const streamDocument = useDocument(); + const { t, i18n } = useTranslation(); + + const coordinate = resolveComponentData( + data.coordinate, + i18n.language, + streamDocument + ) as Coordinate | undefined; + const address = resolveComponentData( + data.address, + i18n.language, + streamDocument + ) as AddressType | undefined; + const directionsText = resolveComponentData( + data.directionsText, + i18n.language, + streamDocument + ); + + const directionsLink = + address && (address.line1 || address.city || address.region) + ? getDirections(address, undefined, undefined, { provider: "google" }) + : undefined; + + const latitude = coordinate?.latitude ?? 0; + const longitude = coordinate?.longitude ?? 0; + const hasCoordinate = Number.isFinite(latitude) && Number.isFinite(longitude); + const mapImageUrl = + data.apiKey && hasCoordinate + ? `https://api.mapbox.com/styles/v1/mapbox/${styles.mapStyle}/static/pin-l+111(${longitude},${latitude})/${longitude},${latitude},14/1280x720?access_token=${data.apiKey}&logo=false&attribution=false` + : ""; + + return ( +
+ {mapImageUrl ? ( + + {t("map", + + ) : puck.isEditing ? ( +
+ ) : null} + {styles.showDirectionsAction && directionsText && directionsLink ? ( + + {directionsText} + + ) : null} +
+ ); +}; + +export const defaultYetiMapSurfaceSlotProps: YetiMapSurfaceSlotProps = { + data: { + apiKey: "", + coordinate: { + field: "yextDisplayCoordinate", + constantValue: { + latitude: 0, + longitude: 0, + }, + constantValueEnabled: false, + }, + address: { + field: "address", + constantValue: { + line1: "", + city: "", + region: "", + postalCode: "", + countryCode: "US", + }, + constantValueEnabled: false, + }, + directionsText: toTranslatableString("Get Directions"), + }, + styles: { + mapStyle: "streets-v12", + showDirectionsAction: true, + }, +}; + +export const YetiMapSurfaceSlot: ComponentConfig<{ + props: YetiMapSurfaceSlotProps; +}> = { + label: "Yeti Map Surface Slot", + fields, + defaultProps: defaultYetiMapSurfaceSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiNavLinksSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiNavLinksSlot.tsx new file mode 100644 index 0000000000..eb3ffc8a7b --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiNavLinksSlot.tsx @@ -0,0 +1,170 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiNavLinksSlotProps { + data: { + links: Array<{ + label: TranslatableString; + href: string; + openInNewTab: boolean; + }>; + }; + styles: { + align: "left" | "center" | "right"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + links: YextField("Links", { + type: "array", + defaultItemProps: { + label: toTranslatableString("Link"), + href: "#", + openInNewTab: false, + }, + arrayFields: { + label: YextField("Label", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + href: YextField("Href", { type: "text" }), + openInNewTab: YextField("Open in New Tab", { + type: "radio", + options: [ + { label: "No", value: false }, + { label: "Yes", value: true }, + ], + }), + }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + align: YextField("Align", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + { label: "Right", value: "right" }, + ], + }), + }, + }), +}; + +const alignClassMap: Record = + { + left: "justify-start", + center: "justify-center", + right: "justify-end", + }; + +const YetiNavLinksSlotComponent: PuckComponent = ({ + data, + styles, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + return ( +
    + {data.links.map((item, index) => { + const label = resolveComponentData( + item?.label, + i18n.language, + streamDocument + ); + if (!label) { + return null; + } + + return ( +
  • + + {label} + +
  • + ); + })} +
+ ); +}; + +export const defaultYetiPrimaryNavLinksSlotProps: YetiNavLinksSlotProps = { + data: { + links: [ + { + label: toTranslatableString("Shop All"), + href: "https://www.yeti.com/", + openInNewTab: false, + }, + { + label: toTranslatableString("Corporate Sales"), + href: "https://www.yeti.com/corporate-sales.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Customize"), + href: "https://www.yeti.com/customize", + openInNewTab: false, + }, + { + label: toTranslatableString("Resale"), + href: "https://rescues.yeti.com/", + openInNewTab: true, + }, + ], + }, + styles: { + align: "left", + }, +}; + +export const defaultYetiUtilityNavLinksSlotProps: YetiNavLinksSlotProps = { + data: { + links: [ + { + label: toTranslatableString("Find a Store"), + href: "https://www.yeti.com/yeti-store-locations.html", + openInNewTab: false, + }, + { + label: toTranslatableString("Customer Support"), + href: "https://www.yeti.com/help-guide.html", + openInNewTab: false, + }, + ], + }, + styles: { + align: "right", + }, +}; + +export const YetiNavLinksSlot: ComponentConfig<{ + props: YetiNavLinksSlotProps; +}> = { + label: "Yeti Nav Links Slot", + fields, + defaultProps: defaultYetiPrimaryNavLinksSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiParkingSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiParkingSlot.tsx new file mode 100644 index 0000000000..ef00826fa2 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiParkingSlot.tsx @@ -0,0 +1,88 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { YetiParagraph } from "../atoms/YetiParagraph.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiParkingSlotProps { + data: { + heading: TranslatableString; + body: TranslatableString; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + heading: YextField("Heading", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + body: YextField("Body", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), +}; + +const YetiParkingSlotComponent: PuckComponent = ({ + data, +}) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const heading = resolveComponentData( + data.heading, + i18n.language, + streamDocument + ); + const body = resolveComponentData(data.body, i18n.language, streamDocument); + + if (!heading && !body) { + return null; + } + + return ( +
+ {heading ? ( + + {heading} + + ) : null} + {body ? ( + + {body} + + ) : null} +
+ ); +}; + +export const defaultYetiParkingSlotProps: YetiParkingSlotProps = { + data: { + heading: toTranslatableString("Parking"), + body: toTranslatableString( + "Park at a nearby garage or find street parking around the store." + ), + }, +}; + +export const YetiParkingSlot: ComponentConfig<{ props: YetiParkingSlotProps }> = + { + label: "Yeti Parking Slot", + fields, + defaultProps: defaultYetiParkingSlotProps, + render: (props) => , + }; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiPrimaryActionSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiPrimaryActionSlot.tsx new file mode 100644 index 0000000000..4ab21c8f1f --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiPrimaryActionSlot.tsx @@ -0,0 +1,107 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiPrimaryActionSlotProps { + data: { + actionText: TranslatableString; + actionHref: string; + openInNewTab: boolean; + }; + styles: { + variant: "solid" | "outline"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + actionText: YextField("Action Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + actionHref: YextField("Action Href", { type: "text" }), + openInNewTab: YextField("Open in New Tab", { + type: "radio", + options: [ + { label: "No", value: false }, + { label: "Yes", value: true }, + ], + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + variant: YextField("Variant", { + type: "radio", + options: [ + { label: "Solid", value: "solid" }, + { label: "Outline", value: "outline" }, + ], + }), + }, + }), +}; + +const YetiPrimaryActionSlotComponent: PuckComponent< + YetiPrimaryActionSlotProps +> = ({ data, styles }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + + const actionText = resolveComponentData( + data.actionText, + i18n.language, + streamDocument + ); + + if (!actionText) { + return null; + } + + const variantClass = + styles.variant === "outline" + ? "border border-current bg-transparent" + : "bg-black text-white"; + + return ( + + {actionText} + + ); +}; + +export const defaultYetiPrimaryActionSlotProps: YetiPrimaryActionSlotProps = { + data: { + actionText: toTranslatableString("Shop the shop"), + actionHref: + "https://yeti.locally.com/search/search?embed_type=store&store=173976&uri=search&limit=6&host_domain=www.yeti.com", + openInNewTab: false, + }, + styles: { + variant: "outline", + }, +}; + +export const YetiPrimaryActionSlot: ComponentConfig<{ + props: YetiPrimaryActionSlotProps; +}> = { + label: "Yeti Primary Action Slot", + fields, + defaultProps: defaultYetiPrimaryActionSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiPromoBannerSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiPromoBannerSection.tsx new file mode 100644 index 0000000000..62e1a6087c --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiPromoBannerSection.tsx @@ -0,0 +1,294 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiHeroImageSlotProps } from "./YetiHeroImageSlot.tsx"; +import { defaultYetiSectionHeadingSlotProps } from "./YetiSectionHeadingSlot.tsx"; +import { defaultYetiBodyCopySlotProps } from "./YetiBodyCopySlot.tsx"; +import { defaultYetiPrimaryActionSlotProps } from "./YetiPrimaryActionSlot.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiPromoBannerSectionProps { + styles: { + contentAlign: "left" | "center"; + showImage: boolean; + showHeading: boolean; + showBody: boolean; + showPrimaryAction: boolean; + fullBleed: boolean; + }; + slots: { + PromoImageSlot: Slot; + PromoHeadingSlot: Slot; + PromoBodySlot: Slot; + PromoPrimaryActionSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + contentAlign: YextField("Content Align", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + ], + }), + showImage: YextField("Show Image", { + type: "radio", + options: "SHOW_HIDE", + }), + showHeading: YextField("Show Heading", { + type: "radio", + options: "SHOW_HIDE", + }), + showBody: YextField("Show Body", { + type: "radio", + options: "SHOW_HIDE", + }), + showPrimaryAction: YextField("Show Primary Action", { + type: "radio", + options: "SHOW_HIDE", + }), + fullBleed: YextField("Full Bleed", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + PromoImageSlot: { type: "slot" }, + PromoHeadingSlot: { type: "slot" }, + PromoBodySlot: { type: "slot" }, + PromoPrimaryActionSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiPromoBannerSectionComponent: PuckComponent< + YetiPromoBannerSectionProps +> = ({ styles, slots }) => { + const isCentered = styles.contentAlign === "center"; + + return ( + +
+ {styles.showImage ? ( + + ) : ( +
+ )} +
+
+
+ {styles.showHeading ? ( + + ) : null} + {styles.showBody ? ( +
+ +
+ ) : null} + {styles.showPrimaryAction ? ( +
+ +
+ ) : null} +
+
+
+
+ ); +}; + +export const defaultYetiPromoBannerSectionProps: YetiPromoBannerSectionProps = { + styles: { + contentAlign: "left", + showImage: true, + showHeading: true, + showBody: true, + showPrimaryAction: true, + fullBleed: true, + }, + slots: { + PromoImageSlot: [ + { + type: "YetiHeroImageSlot", + props: { + ...defaultYetiHeroImageSlotProps, + data: { + ...defaultYetiHeroImageSlotProps.data, + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/35d6810f223f9fc3/original/240074_PLP_BMD_3-0_Paragraph_Lifestyle_Store_Page_Customize_Desktop.jpg?auto=format,compress", + width: 1920, + height: 1080, + }, + constantValueEnabled: true, + }, + }, + }, + }, + ], + PromoHeadingSlot: [ + { + type: "YetiSectionHeadingSlot", + props: { + ...defaultYetiSectionHeadingSlotProps, + data: { + text: toTranslatableString("CUSTOMIZE IT IN-STORE"), + }, + styles: { + level: 2, + align: "left", + }, + }, + }, + ], + PromoBodySlot: [ + { + type: "YetiBodyCopySlot", + props: { + ...defaultYetiBodyCopySlotProps, + data: { + copy: toTranslatableString( + "Choose from 9 different fonts and 12 design galleries to make your drinkware all your own." + ), + }, + styles: { + align: "left", + }, + }, + }, + ], + PromoPrimaryActionSlot: [ + { + type: "YetiPrimaryActionSlot", + props: { + ...defaultYetiPrimaryActionSlotProps, + data: { + ...defaultYetiPrimaryActionSlotProps.data, + actionText: toTranslatableString("Shop the shop"), + }, + styles: { + variant: "outline", + }, + }, + }, + ], + }, + liveVisibility: true, +}; + +export const defaultYetiReservePromoBannerSectionProps: YetiPromoBannerSectionProps = + { + ...defaultYetiPromoBannerSectionProps, + slots: { + PromoImageSlot: [ + { + type: "YetiHeroImageSlot", + props: { + ...defaultYetiHeroImageSlotProps, + data: { + ...defaultYetiHeroImageSlotProps.data, + image: { + field: "", + constantValue: { + url: "https://yeti-webmedia.imgix.net/m/204eb0035a6bce05/original/240074_PLP_BMD_3-0_Paragraph_Lifestyle_Store_Page_Desktop.jpg?auto=format,compress", + width: 1920, + height: 1080, + }, + constantValueEnabled: true, + }, + }, + }, + }, + ], + PromoHeadingSlot: [ + { + type: "YetiSectionHeadingSlot", + props: { + ...defaultYetiSectionHeadingSlotProps, + data: { + text: toTranslatableString("PUT IT ON ICE"), + }, + styles: { + level: 2, + align: "left", + }, + }, + }, + ], + PromoBodySlot: [ + { + type: "YetiBodyCopySlot", + props: { + ...defaultYetiBodyCopySlotProps, + data: { + copy: toTranslatableString( + "Reserve gear in your local YETI store to pick up when you're ready." + ), + }, + styles: { + align: "left", + }, + }, + }, + ], + PromoPrimaryActionSlot: [ + { + type: "YetiPrimaryActionSlot", + props: { + ...defaultYetiPrimaryActionSlotProps, + data: { + ...defaultYetiPrimaryActionSlotProps.data, + actionText: toTranslatableString("Shop the shop"), + }, + styles: { + variant: "outline", + }, + }, + }, + ], + }, + }; + +export const YetiPromoBannerSection: ComponentConfig<{ + props: YetiPromoBannerSectionProps; +}> = { + label: "Yeti Promo Banner Section", + fields, + defaultProps: defaultYetiPromoBannerSectionProps, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiSectionHeadingSlot.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiSectionHeadingSlot.tsx new file mode 100644 index 0000000000..c70f7c8644 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiSectionHeadingSlot.tsx @@ -0,0 +1,93 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent } from "@puckeditor/core"; +import { + TranslatableString, + YextField, + resolveComponentData, + useDocument, +} from "../ve.ts"; +import { useTranslation } from "react-i18next"; +import { YetiHeading } from "../atoms/YetiHeading.tsx"; +import { toTranslatableString } from "../atoms/defaults.ts"; + +export interface YetiSectionHeadingSlotProps { + data: { + text: TranslatableString; + }; + styles: { + level: 2 | 3 | 4; + align: "left" | "center"; + }; +} + +const fields: Fields = { + data: YextField("Data", { + type: "object", + objectFields: { + text: YextField("Text", { + type: "translatableString", + filter: { types: ["type.string"] }, + }), + }, + }), + styles: YextField("Styles", { + type: "object", + objectFields: { + level: YextField("Level", { + type: "select", + options: [ + { label: "H2", value: 2 }, + { label: "H3", value: 3 }, + { label: "H4", value: 4 }, + ], + }), + align: YextField("Align", { + type: "radio", + options: [ + { label: "Left", value: "left" }, + { label: "Center", value: "center" }, + ], + }), + }, + }), +}; + +const YetiSectionHeadingSlotComponent: PuckComponent< + YetiSectionHeadingSlotProps +> = ({ data, styles }) => { + const streamDocument = useDocument(); + const { i18n } = useTranslation(); + const text = resolveComponentData(data.text, i18n.language, streamDocument); + + if (!text) { + return null; + } + + return ( + + {text} + + ); +}; + +export const defaultYetiSectionHeadingSlotProps: YetiSectionHeadingSlotProps = { + data: { + text: toTranslatableString("Section Heading"), + }, + styles: { + level: 2, + align: "left", + }, +}; + +export const YetiSectionHeadingSlot: ComponentConfig<{ + props: YetiSectionHeadingSlotProps; +}> = { + label: "Yeti Section Heading Slot", + fields, + defaultProps: defaultYetiSectionHeadingSlotProps, + render: (props) => , +}; diff --git a/packages/visual-editor/src/components/custom/yeti/components/YetiStoreInfoSection.tsx b/packages/visual-editor/src/components/custom/yeti/components/YetiStoreInfoSection.tsx new file mode 100644 index 0000000000..bdc7a32cd3 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/components/YetiStoreInfoSection.tsx @@ -0,0 +1,157 @@ +// @ts-nocheck +import { ComponentConfig, Fields, PuckComponent, Slot } from "@puckeditor/core"; +import { YextField } from "../ve.ts"; +import { YetiSectionShell } from "../atoms/YetiSectionShell.tsx"; +import { defaultYetiHoursSlotProps } from "./YetiHoursSlot.tsx"; +import { defaultYetiLocationDetailsSlotProps } from "./YetiLocationDetailsSlot.tsx"; +import { defaultYetiParkingSlotProps } from "./YetiParkingSlot.tsx"; +import { defaultYetiMapSurfaceSlotProps } from "./YetiMapSurfaceSlot.tsx"; + +export interface YetiStoreInfoSectionProps { + styles: { + backgroundClassName: "bg-white" | "bg-neutral-50" | "bg-[#F7F4EE]"; + showHours: boolean; + showLocationInfo: boolean; + showMap: boolean; + showParking: boolean; + }; + slots: { + HoursSlot: Slot; + LocationInfoSlot: Slot; + ParkingSlot: Slot; + MapSlot: Slot; + }; + liveVisibility: boolean; +} + +const fields: Fields = { + styles: YextField("Styles", { + type: "object", + objectFields: { + backgroundClassName: YextField("Background", { + type: "select", + options: [ + { label: "White", value: "bg-white" }, + { label: "Soft Gray", value: "bg-neutral-50" }, + { label: "Warm Neutral", value: "bg-[#F7F4EE]" }, + ], + }), + showHours: YextField("Show Hours", { + type: "radio", + options: "SHOW_HIDE", + }), + showLocationInfo: YextField("Show Location", { + type: "radio", + options: "SHOW_HIDE", + }), + showMap: YextField("Show Map", { + type: "radio", + options: "SHOW_HIDE", + }), + showParking: YextField("Show Parking", { + type: "radio", + options: "SHOW_HIDE", + }), + }, + }), + slots: { + type: "object", + objectFields: { + HoursSlot: { type: "slot" }, + LocationInfoSlot: { type: "slot" }, + ParkingSlot: { type: "slot" }, + MapSlot: { type: "slot" }, + }, + visible: false, + }, + liveVisibility: YextField("Visible on Live Page", { + type: "radio", + options: [ + { label: "Show", value: true }, + { label: "Hide", value: false }, + ], + }), +}; + +const YetiStoreInfoSectionComponent: PuckComponent< + YetiStoreInfoSectionProps +> = ({ styles, slots }) => { + return ( + +
+ {styles.showHours ? ( +
+ +
+ ) : null} + {styles.showLocationInfo ? ( +
+ +
+ ) : null} + {styles.showMap ? ( +
+ +
+ ) : null} + {styles.showParking ? ( +
+ +
+ ) : null} +
+
+ ); +}; + +export const YetiStoreInfoSection: ComponentConfig<{ + props: YetiStoreInfoSectionProps; +}> = { + label: "Yeti Store Info Section", + fields, + defaultProps: { + styles: { + backgroundClassName: "bg-white", + showHours: true, + showLocationInfo: true, + showMap: true, + showParking: true, + }, + slots: { + HoursSlot: [ + { + type: "YetiHoursSlot", + props: defaultYetiHoursSlotProps, + }, + ], + LocationInfoSlot: [ + { + type: "YetiLocationDetailsSlot", + props: defaultYetiLocationDetailsSlotProps, + }, + ], + ParkingSlot: [ + { + type: "YetiParkingSlot", + props: defaultYetiParkingSlotProps, + }, + ], + MapSlot: [ + { + type: "YetiMapSurfaceSlot", + props: defaultYetiMapSurfaceSlotProps, + }, + ], + }, + liveVisibility: true, + }, + render: (props) => { + if (!props.liveVisibility && !props.puck.isEditing) { + return null; + } + return ; + }, +}; diff --git a/packages/visual-editor/src/components/custom/yeti/index.ts b/packages/visual-editor/src/components/custom/yeti/index.ts new file mode 100644 index 0000000000..7618b72a95 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/index.ts @@ -0,0 +1,29 @@ +export { + YetiHeaderSection, + type YetiHeaderSectionProps, +} from "./components/YetiHeaderSection.tsx"; +export { + YetiLocationHeroSection, + type YetiLocationHeroSectionProps, +} from "./components/YetiLocationHeroSection.tsx"; +export { + YetiStoreInfoSection, + type YetiStoreInfoSectionProps, +} from "./components/YetiStoreInfoSection.tsx"; +export { + YetiPromoBannerSection, + defaultYetiReservePromoBannerSectionProps, + type YetiPromoBannerSectionProps, +} from "./components/YetiPromoBannerSection.tsx"; +export { + YetiExploreCarouselSection, + type YetiExploreCarouselSectionProps, +} from "./components/YetiExploreCarouselSection.tsx"; +export { + YetiFaqSection, + type YetiFaqSectionProps, +} from "./components/YetiFaqSection.tsx"; +export { + YetiFooterSection, + type YetiFooterSectionProps, +} from "./components/YetiFooterSection.tsx"; diff --git a/packages/visual-editor/src/components/custom/yeti/ve.ts b/packages/visual-editor/src/components/custom/yeti/ve.ts new file mode 100644 index 0000000000..d060a2ea77 --- /dev/null +++ b/packages/visual-editor/src/components/custom/yeti/ve.ts @@ -0,0 +1,10 @@ +export { EntityField } from "../../../editor/EntityField.tsx"; +export { YextField } from "../../../editor/YextField.tsx"; +export type { YextEntityField } from "../../../editor/yextEntityFieldUtils.ts"; +export { useDocument } from "../../../hooks/useDocument.tsx"; +export type { + TranslatableRichText, + TranslatableString, +} from "../../../types/types.ts"; +export { resolveComponentData } from "../../../utils/resolveComponentData.tsx"; +export { resolveYextEntityField } from "../../../utils/resolveYextEntityField.ts"; diff --git a/packages/visual-editor/src/components/index.ts b/packages/visual-editor/src/components/index.ts index ca30e65530..d6cba2919e 100644 --- a/packages/visual-editor/src/components/index.ts +++ b/packages/visual-editor/src/components/index.ts @@ -25,3 +25,4 @@ export { defaultThemeConfig, createDefaultThemeConfig, } from "./DefaultThemeConfig.ts"; +export * from "./custom/yeti/index.ts"; diff --git a/packages/visual-editor/src/docs/components.md b/packages/visual-editor/src/docs/components.md index a953f742ed..d390a08d99 100644 --- a/packages/visual-editor/src/docs/components.md +++ b/packages/visual-editor/src/docs/components.md @@ -931,3 +931,101 @@ If 'true', the component is visible on the live page; if 'false', it's hidden. | `slots` | `{ SectionHeadingSlot: Slot; VideoSlot: Slot; }` | | | --- + +## YetiExploreCarouselSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :--------------------------------------------------------------------------------------------------- | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ HeadingSlot: Slot; CardsSlot: Slot; }` | | | +| `styles` | `{ backgroundClassName: "bg-white" \| "bg-neutral-100"; showHeading: boolean; showCards: boolean; }` | | | + +--- + +## YetiFaqSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :----------------------------------------------------------------------------------------------------- | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ HeadingSlot: Slot; FaqListSlot: Slot; }` | | | +| `styles` | `{ backgroundClassName: "bg-white" \| "bg-neutral-100"; showHeading: boolean; showFaqList: boolean; }` | | | + +--- + +## YetiFooterSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ FooterLayoutSlot: Slot; }` | | | +| `styles` | `{ backgroundClassName: "bg-neutral-100" \| "bg-white" \| "bg-[#0F3658]"; textClassName: "text-neutral-900" \| "text-white"; dividerClassName: "border-black/15" \| "border-white/30"; showTopDivider: boolean; showFooterLayout: boolean; }` | | | + +--- + +## YetiHeaderSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ HeaderLayoutSlot: Slot; }` | | | +| `styles` | `{ backgroundClassName: "bg-neutral-100" \| "bg-white" \| "bg-[#0F3658]"; textClassName: "text-neutral-900" \| "text-white"; dividerClassName: "border-black/15" \| "border-white/30"; showBottomDivider: boolean; showHeaderLayout: boolean; }` | | | + +--- + +## YetiLocationHeroSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :------------------------------------------------------------------------------------------------------------------------ | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ HeroImageSlot: Slot; HeroHeadingSlot: Slot; HeroBodySlot: Slot; HeroActionSlot: Slot; }` | | | +| `styles` | `{ contentAlign: "left" \| "center"; showImage: boolean; showHeading: boolean; showBody: boolean; showAction: boolean; }` | | | + +--- + +## YetiPromoBannerSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ PromoImageSlot: Slot; PromoHeadingSlot: Slot; PromoBodySlot: Slot; PromoPrimaryActionSlot: Slot; }` | | | +| `styles` | `{ contentAlign: "left" \| "center"; showImage: boolean; showHeading: boolean; showBody: boolean; showPrimaryAction: boolean; fullBleed: boolean; }` | | | + +--- + +## YetiStoreInfoSection + +### Props + +#### Other Props + +| Prop | Type | Description | Default | +| :--------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------- | :---------- | :------ | +| `liveVisibility` | `boolean` | | | +| `slots` | `{ HoursSlot: Slot; LocationInfoSlot: Slot; ParkingSlot: Slot; MapSlot: Slot; }` | | | +| `styles` | `{ backgroundClassName: "bg-white" \| "bg-neutral-50" \| "bg-[#F7F4EE]"; showHours: boolean; showLocationInfo: boolean; showMap: boolean; showParking: boolean; }` | | | + +--- diff --git a/starter/src/templates/yeti/yeti-config.tsx b/starter/src/templates/yeti/yeti-config.tsx new file mode 100644 index 0000000000..529c853380 --- /dev/null +++ b/starter/src/templates/yeti/yeti-config.tsx @@ -0,0 +1,40 @@ +import { Config, DropZone } from "@puckeditor/core"; +import { + YetiSectionsCategory, + YetiSectionsCategoryComponents, + YetiSectionsCategoryProps, +} from "@yext/visual-editor"; +import { + YetiSlotsCategory, + YetiSlotsCategoryComponents, + YetiSlotsCategoryProps, +} from "@yext/visual-editor"; + +export interface YetiTemplateProps + extends YetiSectionsCategoryProps, + YetiSlotsCategoryProps {} + +export const yetiConfig: Config = { + components: { + ...YetiSectionsCategoryComponents, + ...YetiSlotsCategoryComponents, + }, + categories: { + yetiSections: { + title: "Yeti Sections", + components: YetiSectionsCategory, + }, + yetiSlots: { + components: YetiSlotsCategory, + visible: false, + }, + }, + root: { + render: () => ( + + ), + }, +}; diff --git a/starter/src/templates/yeti/yeti-template.tsx b/starter/src/templates/yeti/yeti-template.tsx new file mode 100644 index 0000000000..c1b0b449fa --- /dev/null +++ b/starter/src/templates/yeti/yeti-template.tsx @@ -0,0 +1,160 @@ +import "@yext/visual-editor/editor.css"; +import { + GetHeadConfig, + GetPath, + HeadConfig, + Template, + TemplateConfig, + TemplateProps, + TemplateRenderProps, + TransformProps, +} from "@yext/pages"; +import React from "react"; +import { Render } from "@puckeditor/core"; +import { SchemaWrapper } from "@yext/pages-components"; +import { + VisualEditorProvider, + applyTheme, + defaultThemeConfig, + applyAnalytics, + applyHeaderScript, + getSchema, + injectTranslations, + YetiHeaderSection, + YetiLocationHeroSection, + YetiStoreInfoSection, + YetiPromoBannerSection, + defaultYetiReservePromoBannerSectionProps, + YetiExploreCarouselSection, + YetiFaqSection, + YetiFooterSection, +} from "@yext/visual-editor"; +import { yetiConfig } from "./yeti-config"; + +export const config = { + name: "yeti-location", + stream: { + $id: "yeti-location-stream", + filter: { + entityTypes: ["location"], + }, + fields: [ + "id", + "uid", + "meta", + "slug", + "locale", + "name", + "address", + "mainPhone", + "hours", + "yextDisplayCoordinate", + ], + localization: { + locales: ["en"], + }, + }, + additionalProperties: { + isVETemplate: true, + }, +} as const satisfies TemplateConfig; + +export const getHeadConfig: GetHeadConfig = ( + data, +): HeadConfig => { + const { document, relativePrefixToRoot } = data; + const schema = getSchema(data); + + return { + title: document.name, + charset: "UTF-8", + viewport: "width=device-width, initial-scale=1", + tags: [ + { + type: "link", + attributes: { + rel: "icon", + type: "image/x-icon", + }, + }, + ], + other: [ + applyAnalytics(document), + applyHeaderScript(document), + applyTheme(document, relativePrefixToRoot, defaultThemeConfig), + SchemaWrapper(schema), + ].join("\n"), + }; +}; + +export const transformProps: TransformProps> = async ( + data, +) => { + const translations = await injectTranslations(data.document); + return { ...data, translations }; +}; + +export const getPath: GetPath = ({ document }) => { + const localePath = document.locale !== "en" ? `${document.locale}/` : ""; + return document.address + ? `${localePath}${document.address.region}/${document.address.city}/${document.address.line1}-${document.id.toString()}` + : `${localePath}${document.id.toString()}`; +}; + +const defaultLayoutData = { + root: { props: {} }, + content: [ + { + type: "YetiHeaderSection", + props: YetiHeaderSection.defaultProps, + }, + { + type: "YetiLocationHeroSection", + props: YetiLocationHeroSection.defaultProps, + }, + { + type: "YetiStoreInfoSection", + props: YetiStoreInfoSection.defaultProps, + }, + { + type: "YetiPromoBannerSection", + props: YetiPromoBannerSection.defaultProps, + }, + { + type: "YetiExploreCarouselSection", + props: YetiExploreCarouselSection.defaultProps, + }, + { + type: "YetiPromoBannerSection", + props: defaultYetiReservePromoBannerSectionProps, + }, + { + type: "YetiFaqSection", + props: YetiFaqSection.defaultProps, + }, + { + type: "YetiFooterSection", + props: YetiFooterSection.defaultProps, + }, + ], +}; + +const YetiLocationTemplate: Template = (props) => { + const { document } = props; + + const layoutData = + (document as { __?: { layout?: typeof defaultLayoutData } }).__?.layout ?? + defaultLayoutData; + + return ( + + + + ); +}; + +export default YetiLocationTemplate; diff --git a/starter/src/ve.config.tsx b/starter/src/ve.config.tsx index dc5ce337a7..249b5f53a8 100644 --- a/starter/src/ve.config.tsx +++ b/starter/src/ve.config.tsx @@ -8,6 +8,7 @@ import { MainConfigProps, mainConfig, } from "@yext/visual-editor"; +import { yetiConfig } from "./templates/yeti/yeti-config"; interface DevProps extends MainConfigProps, DirectoryCategoryProps {} @@ -28,6 +29,7 @@ export const devConfig: Config = { root: mainConfig.root, }; -export const componentRegistry: Record> = { +export const componentRegistry: Record> = { dev: devConfig, + "yeti-location": yetiConfig, };