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
-
-
{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: {