From 7bb5a84e4983b302e2a1183cdaf867bf70b4d519 Mon Sep 17 00:00:00 2001 From: Mathias Oterhals Myklebust <24361490+mathiazom@users.noreply.github.com> Date: Fri, 18 Oct 2024 06:33:13 +0000 Subject: [PATCH] v3 - customer case detail and landing page (#793) * feat: remove global :focus * feat: update color palette * feat(SanityImage): alternatives to useNextSanityGlobalImage If the project is known, the image url should be used directly without the existence check * feat(SanityImage): blurDataURL * feat(customerCase): remove root rich text field * feat: initial customer case detail page * feat(customerCases): initial customer cases landing page --- src/app/(main)/[lang]/[...path]/page.tsx | 8 +- .../customerCases/CustomerCases.tsx | 48 +++++----- .../customerCase/CustomerCase.tsx | 90 +++++++++++++++++++ .../customerCase/customerCase.module.css | 70 +++++++++++++++ .../customerCases/customerCases.module.css | 24 ++++- src/components/image/SanityImage.tsx | 29 +++++- src/styles/global.css | 11 +-- studio/lib/interfaces/media.ts | 3 + studioShared/lib/interfaces/customerCases.ts | 11 ++- studioShared/lib/interfaces/imageBlock.ts | 7 ++ studioShared/lib/interfaces/richTextBlock.ts | 7 ++ studioShared/lib/queries/customerCases.ts | 23 ++--- .../schemas/documents/customerCase.ts | 7 +- 13 files changed, 280 insertions(+), 58 deletions(-) create mode 100644 src/components/customerCases/customerCase/CustomerCase.tsx create mode 100644 src/components/customerCases/customerCase/customerCase.module.css create mode 100644 studioShared/lib/interfaces/imageBlock.ts create mode 100644 studioShared/lib/interfaces/richTextBlock.ts diff --git a/src/app/(main)/[lang]/[...path]/page.tsx b/src/app/(main)/[lang]/[...path]/page.tsx index 628c964e0..ecc4a66f5 100644 --- a/src/app/(main)/[lang]/[...path]/page.tsx +++ b/src/app/(main)/[lang]/[...path]/page.tsx @@ -2,6 +2,7 @@ import { Metadata } from "next"; import Compensations from "src/components/compensations/Compensations"; import CompensationsPreview from "src/components/compensations/CompensationsPreview"; +import CustomerCase from "src/components/customerCases/customerCase/CustomerCase"; import CustomerCases from "src/components/customerCases/CustomerCases"; import CustomerCasesPreview from "src/components/customerCases/CustomerCasesPreview"; import CustomErrorMessage from "src/components/customErrorMessage/CustomErrorMessage"; @@ -122,12 +123,7 @@ async function Page({ params }: Props) { ); case "customerCase": - return ( - // TODO: implement customer case detail page -
-                  {JSON.stringify(pageData, null, 2)}
-                
- ); + return ; case "legalDocument": return isDraftMode ? ( diff --git a/src/components/customerCases/CustomerCases.tsx b/src/components/customerCases/CustomerCases.tsx index c1dd27bf7..1ca7f3e36 100644 --- a/src/components/customerCases/CustomerCases.tsx +++ b/src/components/customerCases/CustomerCases.tsx @@ -1,5 +1,6 @@ import Link from "next/link"; +import { SanitySharedImage } from "src/components/image/SanityImage"; import LinkButton from "src/components/linkButton/LinkButton"; import Text from "src/components/text/Text"; import { sharedCustomerCasesLink } from "src/components/utils/linkTypes"; @@ -28,27 +29,34 @@ const CustomerCases = async ({ customerCasesPage }: CustomerCasesProps) => { return (
- {customerCasesPage.basicTitle} - {sharedCustomerCases && sharedCustomerCases.data.length > 0 ? ( - sharedCustomerCases.data.map((customerCase) => ( -
- - {customerCase.basicTitle} - - {customerCase.description && ( - {customerCase.description} - )} +
+ {customerCasesPage.basicTitle} + {sharedCustomerCases && sharedCustomerCases.data.length > 0 ? ( + sharedCustomerCases.data.map((customerCase) => ( +
+
+ +
+
+ + {customerCase.basicTitle} + + {customerCase.description && ( + {customerCase.description} + )} +
+
+ )) + ) : ( +
+ + It looks like you haven't created any customer cases yet. + Please visit the shared studio to add some. + +
- )) - ) : ( -
- - It looks like you haven't created any customer cases yet. - Please visit the shared studio to add some. - - -
- )} + )} +
); }; diff --git a/src/components/customerCases/customerCase/CustomerCase.tsx b/src/components/customerCases/customerCase/CustomerCase.tsx new file mode 100644 index 000000000..85b5a759d --- /dev/null +++ b/src/components/customerCases/customerCase/CustomerCase.tsx @@ -0,0 +1,90 @@ +import { SanitySharedImage } from "src/components/image/SanityImage"; +import { RichText } from "src/components/richText/RichText"; +import Text from "src/components/text/Text"; +import { CustomerCase as CustomerCaseDocument } from "studioShared/lib/interfaces/customerCases"; + +import styles from "./customerCase.module.css"; + +export interface CustomerCaseProps { + customerCase: CustomerCaseDocument; +} + +export default function CustomerCase({ customerCase }: CustomerCaseProps) { + return ( +
+
+ + {customerCase.basicTitle} + +
+ +
+
+ {customerCase.description} +
+
+ Kunde + + {customerCase.projectInfo.customer} + +
+
+ Prosjekt + + {customerCase.projectInfo.name} + +
+
+ Varighet + + {customerCase.projectInfo.duration} + +
+
+
+
+ Bransje + + {customerCase.projectInfo.sector} + +
+
+ Leveranse + + {customerCase.projectInfo.delivery} + +
+
+ Konsulenter + + {customerCase.projectInfo.consultants.join(", ")} + +
+
+
+
+ {customerCase.sections.map((section) => ( +
+ {section._type === "richTextBlock" ? ( + + ) : ( +
+ {section.images.map((image) => ( +
+
+ +
+
+ ))} +
+ )} +
+ ))} +
+
+
+ ); +} diff --git a/src/components/customerCases/customerCase/customerCase.module.css b/src/components/customerCases/customerCase/customerCase.module.css new file mode 100644 index 000000000..3d8bfbc96 --- /dev/null +++ b/src/components/customerCases/customerCase/customerCase.module.css @@ -0,0 +1,70 @@ +.wrapper { + display: flex; + flex-direction: column; + margin: 8rem 0; + align-items: center; +} + +.content { + display: flex; + flex-direction: column; + max-width: 1200px; + gap: 2rem; + padding: 1rem; +} + +.mainTitle { + font-weight: 600; +} + +.mainImageWrapper img { + border-radius: 12px; +} + +.ingress { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.projectInfo { + display: flex; + gap: 1rem; + flex-grow: 1; +} + +.projectInfoItem { + display: flex; + gap: 1rem; +} + +.projectInfoItemValue { + font-weight: 300; + white-space: nowrap; +} + +.sectionsWrapper { + display: flex; + flex-direction: column; + gap: 2rem; +} + +.imageBlockWrapper { + display: flex; + flex-direction: column; + gap: 1rem; +} + +.imageBlockImageWrapper { + display: flex; + flex-direction: column; + align-items: center; +} + +.imageBlockImageContent { + max-width: 800px; +} + +.imageBlockImageContent img { + border-radius: 12px; +} diff --git a/src/components/customerCases/customerCases.module.css b/src/components/customerCases/customerCases.module.css index 119bf3d06..4820c2987 100644 --- a/src/components/customerCases/customerCases.module.css +++ b/src/components/customerCases/customerCases.module.css @@ -1,8 +1,16 @@ .wrapper { display: flex; flex-direction: column; - padding: 10rem 5rem; + margin: 8rem 0; + align-items: center; +} + +.content { + display: flex; + flex-direction: column; gap: 5rem; + max-width: 1200px; + padding: 1rem; } .section { @@ -10,3 +18,17 @@ flex-direction: column; gap: 5rem; } + +.caseWrapper { + display: flex; + gap: 3rem; +} + +.caseImageWrapper { + min-width: 20rem; + max-width: 20rem; +} + +.caseImageWrapper img { + border-radius: 12px; +} diff --git a/src/components/image/SanityImage.tsx b/src/components/image/SanityImage.tsx index d5ba43089..676c8279d 100644 --- a/src/components/image/SanityImage.tsx +++ b/src/components/image/SanityImage.tsx @@ -36,8 +36,13 @@ const useNextSanityGlobalImage = ( return globalImage; }; -const SanityAssetImage = ({ image }: { image: IImage }) => { - const imageProps = useNextSanityGlobalImage(image); +const SanityAssetImage = ({ + image, + imageProps, +}: { + image: IImage; + imageProps?: UseNextSanityImageProps; +}) => { const objectPosition = image.hotspot ? `${image.hotspot.x * 100}% ${image.hotspot.y * 100}%` : "50% 50%"; // Default to center if no hotspot is defined @@ -50,6 +55,7 @@ const SanityAssetImage = ({ image }: { image: IImage }) => { {...imageProps} width={imageProps.width} height={imageProps.height} + blurDataURL={image.metadata?.lqip} style={{ objectFit: "cover", objectPosition, @@ -62,6 +68,23 @@ const SanityAssetImage = ({ image }: { image: IImage }) => { ); }; +export function SanityStudioImage({ image }: { image: IImage }) { + const imageProps = useNextSanityImage(client, image); + return ; +} + +export function SanitySharedImage({ image }: { image: IImage }) { + const imageProps = useNextSanityImage(sharedClient, image); + return ; +} + +function SanityGlobalImage({ image }: { image: IImage }) { + const imageProps = useNextSanityGlobalImage(image); + return ( + + ); +} + export function SanityImage({ image }: { image: IImage }) { if (image?.src) { return ( @@ -74,5 +97,5 @@ export function SanityImage({ image }: { image: IImage }) { /> ); } - return ; + return ; } diff --git a/src/styles/global.css b/src/styles/global.css index bb4cac2ec..289b43e12 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -12,12 +12,12 @@ html { --primary-dark: #e61a6b; --primary-darker: #8b0f40; --primary-light: #f5a4c4; - --primary-bg: #f4f1e7; - --primary-bg-dark: #fad2e2; + --primary-bg: #f2f2f2; + --primary-bg-dark: #d9d9d9; --primary-white-bright: #ffffff; --primary-white: #faf8f5; - --primary-black: #1f1f1f; + --primary-black: #2d2d2d; --secondary-off-white1: #f4efe8; --secondary-off-white2: #ece1d3; @@ -37,11 +37,6 @@ html { --max-content-width-small: 1000px; } -:focus { - outline: 2px solid var(--focus-color); - border-radius: 4px; -} - body { margin: 0; padding: 0; diff --git a/studio/lib/interfaces/media.ts b/studio/lib/interfaces/media.ts index 1489c013c..50e11a647 100644 --- a/studio/lib/interfaces/media.ts +++ b/studio/lib/interfaces/media.ts @@ -24,6 +24,9 @@ export interface IImage { alt?: string; crop?: ICrop; hotspot?: IHotspot; + metadata?: { + lqip: string; + }; } export interface ImageExtendedProps extends IImage { diff --git a/studioShared/lib/interfaces/customerCases.ts b/studioShared/lib/interfaces/customerCases.ts index 431796f47..add0405ac 100644 --- a/studioShared/lib/interfaces/customerCases.ts +++ b/studioShared/lib/interfaces/customerCases.ts @@ -1,7 +1,8 @@ -import { PortableTextBlock } from "sanity"; - import { IImage } from "studio/lib/interfaces/media"; +import { ImageBlock } from "./imageBlock"; +import { RichTextBlock } from "./richTextBlock"; + export interface CustomerCaseProjectInfo { customer: string; name: string; @@ -17,10 +18,12 @@ export interface CustomerCaseBase { slug: string; basicTitle: string; description: string; + image: IImage; } +export type CustomerCaseSections = (RichTextBlock | ImageBlock)[]; + export interface CustomerCase extends CustomerCaseBase { - image: IImage; - richText: PortableTextBlock[]; projectInfo: CustomerCaseProjectInfo; + sections: CustomerCaseSections; } diff --git a/studioShared/lib/interfaces/imageBlock.ts b/studioShared/lib/interfaces/imageBlock.ts new file mode 100644 index 000000000..13b6ed562 --- /dev/null +++ b/studioShared/lib/interfaces/imageBlock.ts @@ -0,0 +1,7 @@ +import { IImage } from "studio/lib/interfaces/media"; + +export interface ImageBlock { + _key: string; + _type: "imageBlock"; + images: IImage[]; +} diff --git a/studioShared/lib/interfaces/richTextBlock.ts b/studioShared/lib/interfaces/richTextBlock.ts new file mode 100644 index 000000000..330201a4e --- /dev/null +++ b/studioShared/lib/interfaces/richTextBlock.ts @@ -0,0 +1,7 @@ +import { PortableTextBlock } from "sanity"; + +export interface RichTextBlock { + _key: string; + _type: "richTextBlock"; + richText: PortableTextBlock[]; +} diff --git a/studioShared/lib/queries/customerCases.ts b/studioShared/lib/queries/customerCases.ts index 28fbdb46c..a9647f1fb 100644 --- a/studioShared/lib/queries/customerCases.ts +++ b/studioShared/lib/queries/customerCases.ts @@ -3,12 +3,23 @@ import { groq } from "next-sanity"; import { LANGUAGE_FIELD_FRAGMENT } from "studio/lib/queries/i18n"; import { translatedFieldFragment } from "studio/lib/queries/utils/i18n"; +const INTERNATIONALIZED_IMAGE_FRAGMENT = groq` + asset, + "metadata": asset -> metadata { + lqip + }, + "alt": ${translatedFieldFragment("alt")} +`; + const CUSTOMER_CASE_BASE_FRAGMENT = groq` _id, ${LANGUAGE_FIELD_FRAGMENT}, "slug": ${translatedFieldFragment("slug")}, "basicTitle": ${translatedFieldFragment("basicTitle")}, - "description": ${translatedFieldFragment("description")} + "description": ${translatedFieldFragment("description")}, + "image": image { + ${INTERNATIONALIZED_IMAGE_FRAGMENT} + } `; export const CUSTOMER_CASES_QUERY = groq` @@ -17,18 +28,9 @@ export const CUSTOMER_CASES_QUERY = groq` } `; -const INTERNATIONALIZED_IMAGE_FRAGMENT = groq` - asset, - "alt": ${translatedFieldFragment("alt")} -`; - export const CUSTOMER_CASE_QUERY = groq` *[_type == "customerCase" && ${translatedFieldFragment("slug")} == $slug][0] { ${CUSTOMER_CASE_BASE_FRAGMENT}, - "image": image { - ${INTERNATIONALIZED_IMAGE_FRAGMENT} - }, - "richText": ${translatedFieldFragment("richText")}, "projectInfo": projectInfo { customer, "name": ${translatedFieldFragment("name")}, @@ -38,6 +40,7 @@ export const CUSTOMER_CASE_QUERY = groq` consultants }, "sections": sections[] { + _key, _type, _type == "richTextBlock" => { "richText": ${translatedFieldFragment("richText")}, diff --git a/studioShared/schemas/documents/customerCase.ts b/studioShared/schemas/documents/customerCase.ts index b641e7df5..5e854614e 100644 --- a/studioShared/schemas/documents/customerCase.ts +++ b/studioShared/schemas/documents/customerCase.ts @@ -2,7 +2,7 @@ import { defineField, defineType } from "sanity"; import { isInternationalizedString } from "studio/lib/interfaces/global"; import { internationalizedImage } from "studio/schemas/fields/media"; -import { richTextID, titleID } from "studio/schemas/fields/text"; +import { titleID } from "studio/schemas/fields/text"; import { titleSlug } from "studio/schemas/schemaTypes/slug"; import { firstTranslation } from "studio/utils/i18n"; import { customerCaseProjectInfo } from "studioShared/schemas/fields/customerCaseProjectInfo"; @@ -51,11 +51,6 @@ const customerCase = defineType({ type: "array", of: [richTextBlock, imageBlock, listBlock], }), - defineField({ - name: richTextID, - title: "Body", - type: "internationalizedArrayRichText", - }), ], preview: { select: {