diff --git a/src/components/customerCases/customerCase/CustomerCase.tsx b/src/components/customerCases/customerCase/CustomerCase.tsx
index 9b61adce6..a1ae4e072 100644
--- a/src/components/customerCases/customerCase/CustomerCase.tsx
+++ b/src/components/customerCases/customerCase/CustomerCase.tsx
@@ -3,47 +3,13 @@ import Text from "src/components/text/Text";
import { fetchEmployeesByEmails } from "src/utils/employees";
import {
CustomerCase as CustomerCaseDocument,
- CustomerCaseSection as CustomerCaseSectionObject,
Delivery,
} from "studioShared/lib/interfaces/customerCases";
import styles from "./customerCase.module.css";
import FeaturedCases from "./featuredCases/FeaturedCases";
import CustomerCaseConsultants from "./sections/customerCaseConsultants/CustomerCaseConsultants";
-import ImageSection from "./sections/image/ImageSection";
-import RichTextSection from "./sections/richText/RichTextSection";
-
-function CustomerCaseSection({
- section,
-}: {
- section: CustomerCaseSectionObject;
-}) {
- switch (section._type) {
- case "richTextBlock":
- return ;
- case "quoteBlock":
- return (
- section.quote && (
-
-
-
- {"“"}
- {section.quote}
- {"”"}
-
- {section.author && (
- - {section.author}
- )}
-
-
- )
- );
- case "imageBlock":
- return ;
- }
-}
+import { CustomerCaseSection } from "./sections/CustomerCaseSection";
export interface CustomerCaseProps {
customerCase: CustomerCaseDocument;
diff --git a/src/components/customerCases/customerCase/customerCase.module.css b/src/components/customerCases/customerCase/customerCase.module.css
index 8f8901922..647ecc9f5 100644
--- a/src/components/customerCases/customerCase/customerCase.module.css
+++ b/src/components/customerCases/customerCase/customerCase.module.css
@@ -31,6 +31,7 @@
width: 100%;
height: 36.5rem;
overflow: hidden;
+ padding-top: 1.5rem;
padding-bottom: 2rem;
}
@@ -109,30 +110,3 @@
flex-direction: column;
gap: 4.5rem;
}
-
-.quoteBlock {
- align-self: center;
- max-width: 960px;
- width: 100%;
- border: 2px solid var(--primary-yellow-warning);
- border-radius: 0.5rem;
- background-color: var(--primary-yellow-warning);
-}
-
-.quoteBlockInner {
- display: flex;
- flex-direction: column;
- background-color: var(--primary-bg);
- border-radius: 2rem;
- gap: 1.25rem;
- width: 100%;
- align-items: center;
-}
-
-.withAuthor {
- padding: 4.5rem 0.5rem 3.5rem 0.5rem;
-}
-
-.withoutAuthor {
- padding: 4.5rem 0.5rem;
-}
diff --git a/src/components/customerCases/customerCase/sections/CustomerCaseSection.tsx b/src/components/customerCases/customerCase/sections/CustomerCaseSection.tsx
new file mode 100644
index 000000000..052c7a06f
--- /dev/null
+++ b/src/components/customerCases/customerCase/sections/CustomerCaseSection.tsx
@@ -0,0 +1,23 @@
+import { CustomerCaseSection as CustomerCaseSectionObject } from "studioShared/lib/interfaces/customerCases";
+
+import ImageSection from "./image/ImageSection";
+import QuoteBlock from "./quote/QuoteBlock";
+import RichTextSection from "./richText/RichTextSection";
+import SplitSection from "./splitSection/SplitSection";
+
+export function CustomerCaseSection({
+ section,
+}: {
+ section: CustomerCaseSectionObject;
+}) {
+ switch (section._type) {
+ case "splitSection":
+ return ;
+ case "richTextBlock":
+ return ;
+ case "quoteBlock":
+ return ;
+ case "imageBlock":
+ return ;
+ }
+}
diff --git a/src/components/customerCases/customerCase/sections/quote/QuoteBlock.tsx b/src/components/customerCases/customerCase/sections/quote/QuoteBlock.tsx
new file mode 100644
index 000000000..ce16e0ae6
--- /dev/null
+++ b/src/components/customerCases/customerCase/sections/quote/QuoteBlock.tsx
@@ -0,0 +1,29 @@
+import Text from "src/components/text/Text";
+import { QuoteBlock as QuoteBlockObject } from "studioShared/lib/interfaces/quoteBlock";
+
+import styles from "./quoteBlock.module.css";
+
+export interface QuoteBlockProps {
+ section: QuoteBlockObject;
+}
+
+export default function QuoteBlock({ section }: QuoteBlockProps) {
+ return (
+ section.quote && (
+
+
+
+ {"“"}
+ {section.quote}
+ {"”"}
+
+ {section.author && (
+ - {section.author}
+ )}
+
+
+ )
+ );
+}
diff --git a/src/components/customerCases/customerCase/sections/quote/quoteBlock.module.css b/src/components/customerCases/customerCase/sections/quote/quoteBlock.module.css
new file mode 100644
index 000000000..5461c16b8
--- /dev/null
+++ b/src/components/customerCases/customerCase/sections/quote/quoteBlock.module.css
@@ -0,0 +1,26 @@
+.quoteBlock {
+ align-self: center;
+ max-width: 960px;
+ width: 100%;
+ border: 2px solid var(--primary-yellow-warning);
+ border-radius: 0.5rem;
+ background-color: var(--primary-yellow-warning);
+}
+
+.quoteBlockInner {
+ display: flex;
+ flex-direction: column;
+ background-color: var(--primary-bg);
+ border-radius: 2rem;
+ gap: 1.25rem;
+ width: 100%;
+ align-items: center;
+}
+
+.withAuthor {
+ padding: 4.5rem 0.5rem 3.5rem 0.5rem;
+}
+
+.withoutAuthor {
+ padding: 4.5rem 0.5rem;
+}
diff --git a/src/components/customerCases/customerCase/sections/splitSection/SplitSection.tsx b/src/components/customerCases/customerCase/sections/splitSection/SplitSection.tsx
new file mode 100644
index 000000000..12ac8ccb3
--- /dev/null
+++ b/src/components/customerCases/customerCase/sections/splitSection/SplitSection.tsx
@@ -0,0 +1,35 @@
+import { CustomerCaseSection } from "src/components/customerCases/customerCase/sections/CustomerCaseSection";
+import {
+ SplitSection as SplitSectionObject,
+ SplitSectionSection as SplitSectionSectionObject,
+} from "studioShared/lib/interfaces/splitSection";
+
+import styles from "./splitSection.module.css";
+
+export interface SplitSectionProps {
+ section: SplitSectionObject;
+}
+
+function SplitSectionSection({
+ section,
+}: {
+ section: SplitSectionSectionObject;
+}) {
+ switch (section._type) {
+ case "emptySection":
+ return ;
+ }
+ return ;
+}
+
+export default function SplitSection({ section }: SplitSectionProps) {
+ return (
+
+
+ {section.sections.map((section) => (
+
+ ))}
+
+
+ );
+}
diff --git a/src/components/customerCases/customerCase/sections/splitSection/splitSection.module.css b/src/components/customerCases/customerCase/sections/splitSection/splitSection.module.css
new file mode 100644
index 000000000..68b56e4a4
--- /dev/null
+++ b/src/components/customerCases/customerCase/sections/splitSection/splitSection.module.css
@@ -0,0 +1,28 @@
+.wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.content {
+ width: 100%;
+ max-width: 960px;
+ display: flex;
+ justify-content: space-evenly;
+ gap: 2rem;
+
+ @media (max-width: 1024px) {
+ flex-direction: column;
+ }
+}
+
+.content > * {
+ flex-basis: 0;
+ flex-grow: 1;
+}
+
+.emptySection {
+ @media (max-width: 1024px) {
+ display: none;
+ }
+}
diff --git a/src/components/text/text.module.css b/src/components/text/text.module.css
index 0f0db9350..71789029a 100644
--- a/src/components/text/text.module.css
+++ b/src/components/text/text.module.css
@@ -34,30 +34,17 @@
font-style: normal;
font-weight: 600;
line-height: 120%;
-
- @media (min-width: 1024px) {
- font-size: 4.25rem;
- }
}
.h2 {
font-size: 2.5625rem;
font-weight: 500;
line-height: 110%;
-
- @media (min-width: 1024px) {
- font-size: 3.375;
- line-height: 120%;
- }
}
.h3 {
font-size: 2.125rem;
font-weight: 600;
-
- @media (min-width: 1024px) {
- font-size: 2.25rem;
- }
}
/* TODO: add font variables */
@@ -69,20 +56,11 @@
font-size: 10rem;
line-height: 100%;
font-weight: 500;
-
- @media (min-width: 1024px) {
- font-size: 5.25rem;
- line-height: normal;
- }
}
.bodyXl {
font-size: 2.125rem;
font-weight: 500;
-
- @media (min-width: 1024px) {
- font-size: 2.625rem;
- }
}
.bodyBig {
@@ -90,28 +68,16 @@
font-style: normal;
font-weight: 400;
line-height: 130%;
-
- @media (min-width: 1024px) {
- font-size: 1.875rem;
- }
}
.bodyNormal {
- font-size: 1.25rem;
+ font-size: 20px;
font-weight: 400;
-
- @media (min-width: 1024px) {
- font-size: 1.5rem;
- }
}
.bodySmall {
font-size: 1rem;
font-weight: 400;
-
- @media (min-width: 1024px) {
- font-size: 1.25rem;
- }
}
/* TODO: add font variables */
diff --git a/studio/lib/interfaces/global.ts b/studio/lib/interfaces/global.ts
index dbd856aaa..8b92afc0b 100644
--- a/studio/lib/interfaces/global.ts
+++ b/studio/lib/interfaces/global.ts
@@ -49,3 +49,17 @@ export interface InternationalizedValueRecord {
}
export type InternationalizedString = InternationalizedValueRecord[];
+
+export function isSanityKeyTypeObject(value: unknown): value is {
+ _key: string;
+ _type: string;
+} {
+ return (
+ typeof value === "object" &&
+ value !== null &&
+ "_key" in value &&
+ typeof value._key === "string" &&
+ "_type" in value &&
+ typeof value._type === "string"
+ );
+}
diff --git a/studio/lib/interfaces/richText.ts b/studio/lib/interfaces/richText.ts
index 29ed1ad8f..139225f4b 100644
--- a/studio/lib/interfaces/richText.ts
+++ b/studio/lib/interfaces/richText.ts
@@ -4,6 +4,8 @@ import {
isPortableTextTextBlock,
} from "sanity";
+import { isSanityKeyTypeObject } from "./global";
+
export function isRichText(value: unknown): value is PortableTextBlock[] {
return (
Array.isArray(value) && value.every((item) => isPortableTextBlock(item))
@@ -21,17 +23,3 @@ export function isPortableTextObject(
): value is PortableTextObject {
return isSanityKeyTypeObject(value);
}
-
-function isSanityKeyTypeObject(value: unknown): value is {
- _key: string;
- _type: string;
-} {
- return (
- typeof value === "object" &&
- value !== null &&
- "_key" in value &&
- typeof value._key === "string" &&
- "_type" in value &&
- typeof value._type === "string"
- );
-}
diff --git a/studio/schemas/fields/media.ts b/studio/schemas/fields/media.ts
index 7a6450c9e..cf2078cc7 100644
--- a/studio/schemas/fields/media.ts
+++ b/studio/schemas/fields/media.ts
@@ -60,13 +60,14 @@ export const internationalizedImage = defineField({
media: "asset",
},
prepare({ alt, media }) {
- if (!isInternationalizedString(alt)) {
+ if (alt !== undefined && !isInternationalizedString(alt)) {
throw new TypeError(
`Expected 'alt' to be InternationalizedString, was ${typeof alt}`,
);
}
return {
- title: firstTranslation(alt) ?? undefined,
+ title:
+ alt !== undefined ? (firstTranslation(alt) ?? undefined) : undefined,
media,
};
},
diff --git a/studio/utils/stringUtils.ts b/studio/utils/stringUtils.ts
index dd10ca86d..972ed5cde 100644
--- a/studio/utils/stringUtils.ts
+++ b/studio/utils/stringUtils.ts
@@ -28,4 +28,23 @@ export function normalizeSpaces(string: string): string {
return string.trim().replace(/\s+/g, " ");
}
+/**
+ * Converts a camel cased string (first letter can be either upper- or lowercase),
+ * to a more human-readable string where words are seperated with spaces and
+ * the first letter of each word is in uppercase
+ *
+ * ThisIsTheStringToSplit -> This Is The String To Split
+ * thisIsTheStringToSplit -> This Is The String To Split
+ * thisIsATrickyOne -> This Is A Tricky One
+ *
+ * @param {string} s
+ * @returns {string}
+ */
+export function humanizeCamelCase(s: string): string {
+ if (s.length === 0) {
+ return s;
+ }
+ return (s[0].toUpperCase() + s.slice(1)).split(/(?=[A-Z])/).join(" ");
+}
+
// Add more string utility functions as needed...
diff --git a/studioShared/components/EmptySectionInput.tsx b/studioShared/components/EmptySectionInput.tsx
new file mode 100644
index 000000000..1ca06dfaa
--- /dev/null
+++ b/studioShared/components/EmptySectionInput.tsx
@@ -0,0 +1,14 @@
+"use client";
+
+import { useEffect } from "react";
+import { StringInputProps, set } from "sanity";
+
+export default function EmptySectionInput(props: StringInputProps) {
+ useEffect(() => {
+ // set hidden placeholder field to some arbitrary value
+ // to ensure the section is created
+ props.onChange(set("placeholder"));
+ }, [props]);
+
+ return <>>;
+}
diff --git a/studioShared/lib/interfaces/customerCases.ts b/studioShared/lib/interfaces/customerCases.ts
index 48d14d16c..5fadaaf74 100644
--- a/studioShared/lib/interfaces/customerCases.ts
+++ b/studioShared/lib/interfaces/customerCases.ts
@@ -3,6 +3,7 @@ import { IImage } from "studio/lib/interfaces/media";
import { ImageBlock } from "./imageBlock";
import { QuoteBlock } from "./quoteBlock";
import { RichTextBlock } from "./richTextBlock";
+import { SplitSection } from "./splitSection";
export interface CustomerCaseProjectInfo {
customer: string;
@@ -27,7 +28,9 @@ export interface CustomerCaseBase {
image: IImage;
}
-export type CustomerCaseSection = RichTextBlock | ImageBlock | QuoteBlock;
+export type BaseCustomerCaseSection = RichTextBlock | ImageBlock | QuoteBlock;
+
+export type CustomerCaseSection = BaseCustomerCaseSection | SplitSection;
export interface CustomerCase extends CustomerCaseBase {
projectInfo: CustomerCaseProjectInfo;
diff --git a/studioShared/lib/interfaces/splitSection.ts b/studioShared/lib/interfaces/splitSection.ts
new file mode 100644
index 000000000..82bd0e301
--- /dev/null
+++ b/studioShared/lib/interfaces/splitSection.ts
@@ -0,0 +1,30 @@
+import { isSanityKeyTypeObject } from "studio/lib/interfaces/global";
+import { splitSectionSections } from "studioShared/schemas/objects/splitSection";
+
+import { BaseCustomerCaseSection } from "./customerCases";
+
+export interface EmptySection {
+ _key: string;
+ _type: "emptySection";
+}
+
+export type SplitSectionSection = BaseCustomerCaseSection | EmptySection;
+
+export interface SplitSection {
+ _key: string;
+ _type: "splitSection";
+ sections: SplitSectionSection[];
+}
+
+export function isSplitSectionSections(
+ value: unknown,
+): value is SplitSectionSection[] {
+ return (
+ Array.isArray(value) &&
+ value.every(
+ (item) =>
+ isSanityKeyTypeObject(item) &&
+ splitSectionSections.some((s) => s.name === item._type),
+ )
+ );
+}
diff --git a/studioShared/lib/queries/customerCases.ts b/studioShared/lib/queries/customerCases.ts
index 49f68ed7a..2f8640b37 100644
--- a/studioShared/lib/queries/customerCases.ts
+++ b/studioShared/lib/queries/customerCases.ts
@@ -28,6 +28,28 @@ export const CUSTOMER_CASES_QUERY = groq`
}
`;
+export const BASE_SECTIONS_FRAGMENT = groq`
+ _type == "richTextBlock" => {
+ "richText": ${translatedFieldFragment("richText")},
+ },
+ _type == "imageBlock" => {
+ "images": images[] {
+ ${INTERNATIONALIZED_IMAGE_FRAGMENT}
+ },
+ fullWidth
+ },
+ _type == "listBlock" => {
+ "description": ${translatedFieldFragment("description")},
+ "list": list[] {
+ "text": ${translatedFieldFragment("text")},
+ },
+ },
+ _type == "quoteBlock" => {
+ "quote": ${translatedFieldFragment("quote")},
+ "author": ${translatedFieldFragment("author")},
+ },
+`;
+
export const CUSTOMER_CASE_QUERY = groq`
*[_type == "customerCase" && ${translatedFieldFragment("slug")} == $slug][0] {
${CUSTOMER_CASE_BASE_FRAGMENT},
@@ -44,25 +66,15 @@ export const CUSTOMER_CASE_QUERY = groq`
"sections": sections[] {
_key,
_type,
- _type == "richTextBlock" => {
- "richText": ${translatedFieldFragment("richText")},
- },
- _type == "imageBlock" => {
- "images": images[] {
- ${INTERNATIONALIZED_IMAGE_FRAGMENT}
- },
- fullWidth
- },
- _type == "listBlock" => {
- "description": ${translatedFieldFragment("description")},
- "list": list[] {
- "text": ${translatedFieldFragment("text")},
- },
- },
- _type == "quoteBlock" => {
- "quote": ${translatedFieldFragment("quote")},
- "author": ${translatedFieldFragment("author")},
+ _type == "splitSection" => {
+ "sections": sections[] {
+ _key,
+ _type,
+ _type == "emptySection" => {},
+ ${BASE_SECTIONS_FRAGMENT}
+ }
},
+ ${BASE_SECTIONS_FRAGMENT}
},
"featuredCases": featuredCases[] -> {
${CUSTOMER_CASE_BASE_FRAGMENT}
diff --git a/studioShared/schemas/documents/customerCase.ts b/studioShared/schemas/documents/customerCase.ts
index d9a602a8d..180f295f8 100644
--- a/studioShared/schemas/documents/customerCase.ts
+++ b/studioShared/schemas/documents/customerCase.ts
@@ -8,13 +8,13 @@ import { titleSlug } from "studio/schemas/schemaTypes/slug";
import { buildDraftId, buildPublishedId } from "studio/utils/documentUtils";
import { firstTranslation } from "studio/utils/i18n";
import { customerCaseProjectInfo } from "studioShared/schemas/fields/customerCaseProjectInfo";
-import imageBlock from "studioShared/schemas/objects/imageBlock";
-import listBlock from "studioShared/schemas/objects/listBlock";
-import quoteBlock from "studioShared/schemas/objects/quoteBlock";
-import richTextBlock from "studioShared/schemas/objects/richTextBlock";
+import { baseCustomerCaseSections } from "studioShared/schemas/objects/sections";
+import splitSection from "studioShared/schemas/objects/splitSection";
export const customerCaseID = "customerCase";
+export const customerCaseSections = [...baseCustomerCaseSections, splitSection];
+
const customerCase = defineType({
name: customerCaseID,
type: "document",
@@ -73,7 +73,7 @@ const customerCase = defineType({
title: "Sections",
description: "Add sections here",
type: "array",
- of: [richTextBlock, imageBlock, listBlock, quoteBlock],
+ of: customerCaseSections,
}),
defineField({
name: "featuredCases",
diff --git a/studioShared/schemas/objects/emptySection.ts b/studioShared/schemas/objects/emptySection.ts
new file mode 100644
index 000000000..ebd00bded
--- /dev/null
+++ b/studioShared/schemas/objects/emptySection.ts
@@ -0,0 +1,29 @@
+import { defineField } from "sanity";
+
+import EmptySectionInput from "studioShared/components/EmptySectionInput";
+
+const emptySection = defineField({
+ name: "emptySection",
+ title: "Blank Space",
+ description: "Displays as blank space on the page.",
+ type: "object",
+ fields: [
+ {
+ name: "placeholder",
+ title: "Just some blank space, nothing to see here",
+ type: "string",
+ components: {
+ input: EmptySectionInput,
+ },
+ },
+ ],
+ preview: {
+ prepare() {
+ return {
+ title: "Blank Space",
+ };
+ },
+ },
+});
+
+export default emptySection;
diff --git a/studioShared/schemas/objects/imageBlock.ts b/studioShared/schemas/objects/imageBlock.ts
index a5368e810..715b28058 100644
--- a/studioShared/schemas/objects/imageBlock.ts
+++ b/studioShared/schemas/objects/imageBlock.ts
@@ -33,12 +33,14 @@ const imageBlock = defineField({
let firstImageAlt = null;
if (firstImage !== undefined) {
const imageAlt = firstImage.alt;
- if (!isInternationalizedString(imageAlt)) {
- throw new TypeError(
- `Expected image 'alt' to be InternationalizedString, was ${typeof firstImage.alt}`,
- );
+ if (imageAlt !== undefined) {
+ if (!isInternationalizedString(imageAlt)) {
+ throw new TypeError(
+ `Expected image 'alt' to be InternationalizedString, was ${typeof firstImage.alt}`,
+ );
+ }
+ firstImageAlt = firstTranslation(imageAlt);
}
- firstImageAlt = firstTranslation(imageAlt);
}
return {
title: count > 1 ? `${count} images` : (firstImageAlt ?? undefined),
diff --git a/studioShared/schemas/objects/sections.ts b/studioShared/schemas/objects/sections.ts
new file mode 100644
index 000000000..3158f7214
--- /dev/null
+++ b/studioShared/schemas/objects/sections.ts
@@ -0,0 +1,11 @@
+import imageBlock from "./imageBlock";
+import listBlock from "./listBlock";
+import quoteBlock from "./quoteBlock";
+import richTextBlock from "./richTextBlock";
+
+export const baseCustomerCaseSections = [
+ richTextBlock,
+ imageBlock,
+ listBlock,
+ quoteBlock,
+];
diff --git a/studioShared/schemas/objects/splitSection.ts b/studioShared/schemas/objects/splitSection.ts
new file mode 100644
index 000000000..4d3bdad08
--- /dev/null
+++ b/studioShared/schemas/objects/splitSection.ts
@@ -0,0 +1,44 @@
+import { defineField } from "sanity";
+
+import { humanizeCamelCase } from "studio/utils/stringUtils";
+import { isSplitSectionSections } from "studioShared/lib/interfaces/splitSection";
+
+import emptySection from "./emptySection";
+import { baseCustomerCaseSections } from "./sections";
+
+export const splitSectionSections = [...baseCustomerCaseSections, emptySection];
+
+const splitSection = defineField({
+ name: "splitSection",
+ title: "Split Section",
+ description: "Section containing two other sections, displayed side-by-side",
+ type: "object",
+ fields: [
+ {
+ name: "sections",
+ title: "Sections",
+ type: "array",
+ of: splitSectionSections,
+ validation: (rule) => rule.length(2),
+ },
+ ],
+ preview: {
+ select: {
+ sections: "sections",
+ },
+ prepare({ sections }) {
+ if (!isSplitSectionSections(sections)) {
+ throw new TypeError(
+ "Expected 'sections' to be an array with element type SplitSectionSection.",
+ );
+ }
+ return {
+ title: sections
+ .map((section) => humanizeCamelCase(section._type))
+ .join(" | "),
+ };
+ },
+ },
+});
+
+export default splitSection;