Skip to content

Commit

Permalink
feat: adds contact box section (#928)
Browse files Browse the repository at this point in the history
* feat: adds section schema for contact box

* adds contact box

* makes titles translateable

* feat: adds component and basic functionality for contact box

* feat: adds option for light/dark mode for contact box

* initial unfinished design

* fix: ambiguous syntax

* fix: set contact selector as proper tablist

* feat: adds background dark/light mode to Tag

* fix: sizing for employee card

* fix: skeleton loading with container queries

* fix: responsiveness contact box

* refactor: renames design mode to background to align with sketches

* fix: employee skeleton dark/light background

* fix: simplify email type

* fix: adds description to tag field in contact box
  • Loading branch information
mikaelbr authored Dec 2, 2024
1 parent 2e14e1a commit 513b262
Show file tree
Hide file tree
Showing 14 changed files with 571 additions and 89 deletions.
117 changes: 69 additions & 48 deletions src/components/employeeCard/EmployeeCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,64 +24,85 @@ export default function EmployeeCard({
employee.name &&
employee.email && (
<div className={styles.employeeWrapper}>
<Link
href={`/${language}/${employeePageSlug}/${aliasFromEmail(employee.email)}`}
>
<div className={styles.employeeImage}>
<Image
src={employee.imageUrl}
alt={employee.name}
style={{ objectFit: "cover" }}
fill={true}
/>
</div>
</Link>
<div className={styles.employeeInfoWrapper}>
<Text type="h4" as="h3">
<Link
href={`/${language}/${employeePageSlug}/${aliasFromEmail(employee.email)}`}
className={styles.employeeNameLink}
>
{employee.name}
</Link>
</Text>

<div className={styles.employeeRole}>
{employee.competences.map((competence) => (
<Text
className={styles.employeeRoleDot}
type="labelRegular"
key={competence}
<div className={styles.employeeWrapper__inner}>
<Link
href={`/${language}/${employeePageSlug}/${aliasFromEmail(employee.email)}`}
>
<div className={styles.employeeImage}>
<Image
src={employee.imageUrl}
alt={employee.name}
style={{ objectFit: "cover" }}
fill={true}
/>
</div>
</Link>
<div className={styles.employeeInfoWrapper}>
<Text type="h4" as="h3">
<Link
href={`/${language}/${employeePageSlug}/${aliasFromEmail(employee.email)}`}
className={styles.employeeNameLink}
>
{competence}
</Text>
))}
</div>
{employee.name}
</Link>
</Text>

<Text type="bodyExtraSmall">
<a href={`mailto:${employee.email}`}>{employee.email}</a>
</Text>
{employee.telephone && (
<Text type="bodyExtraSmall">
<a href={`tel:${employee.telephone}`}>
{formatPhoneNumber(employee.telephone)}
</a>
<div className={styles.employeeRole}>
{employee.competences.map((competence) => (
<Text
className={styles.employeeRoleDot}
type="labelRegular"
key={competence}
as="span"
>
{competence}
</Text>
))}
</div>

<Text type="bodyExtraSmall" className={styles.employeeEmail}>
<a href={`mailto:${employee.email}`}>{employee.email}</a>
</Text>
)}
{employee.telephone && (
<Text type="bodyExtraSmall" className={styles.employeePhone}>
<a href={`tel:${employee.telephone}`}>
{formatPhoneNumber(employee.telephone)}
</a>
</Text>
)}
</div>
</div>
</div>
)
);
}

export function EmployeeCardSkeleton() {
export function EmployeeCardSkeleton({
background = "light",
}: {
background?: "light" | "dark";
}) {
const backgroundClass =
background === "dark" ? styles["employeeImage--dark"] : "";
const backgroundClassText =
background === "dark"
? `${styles.skeletonText} ${styles["skeletonText--dark"]}`
: styles.skeletonText;
return (
<div className={`${styles.employeeWrapper} ${styles.skeletonCard}`}>
<div className={styles.employeeImage} />
<div className={`${styles.skeletonText} ${styles.skeletonName}`} />
<div className={`${styles.skeletonText} ${styles.skeletonTitle}`} />
<div className={`${styles.skeletonText} ${styles.skeletonContact}`} />
<div className={`${styles.skeletonText} ${styles.skeletonContact}`} />
<div className={styles.employeeWrapper}>
<div
className={`${styles.employeeWrapper__inner} ${styles.skeletonCard}`}
>
<div className={`${styles.employeeImage} ${backgroundClass}`} />
<div className={styles.employeeInfoWrapper}>
<div className={`${backgroundClassText} ${styles.skeletonName}`} />
<div className={`${backgroundClassText} ${styles.skeletonTitle}`} />
<div
className={`${backgroundClassText} ${styles.skeletonAutoMargin} ${styles.skeletonContact}`}
/>
<div className={`${backgroundClassText} ${styles.skeletonContact}`} />
</div>
</div>
</div>
);
}
73 changes: 47 additions & 26 deletions src/components/employeeCard/employeeCard.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,6 @@
gap: 1rem;
}

.employeeImage {
display: flex;
flex-direction: column;
align-items: center;
background-color: var(--background-bg-dark);
border-radius: 12px;
height: 125px;
width: 50%;
padding: 1rem;
position: relative;
}

.employeeInfoWrapper {
display: flex;
flex-direction: column;
Expand All @@ -32,37 +20,47 @@

.employeeNameLink {
text-decoration: none;
color: currentColor;
}

.employeeEmail a,
.employeePhone a {
color: currentColor;
}

.employeeWrapper {
container-type: inline-size;
container-name: employee;
}
.employeeWrapper__inner {
display: flex;
flex-direction: column;
align-items: flex-start;
min-width: 280px;
max-width: var(--Text-paragraph, 537px);
gap: var(--small, 6px);
gap: 1rem;
row-gap: var(--small, 6px);
}

.employeeInfoWrapper {
display: flex;
text-wrap: wrap;
flex-direction: column;
width: 100%;
height: fit-content;
gap: 0.25rem;
align-self: stretch;
}

.employeeImage {
display: block;
height: 206px;
min-width: 280px;
min-width: 206px;
width: 100%;
max-width: var(--Text-paragraph, 537px);
position: relative;

background-color: var(--background-bg-dark);
border-radius: var(--medium, 12px);
}
.employeeImage--dark {
background-color: var(--background-bg-light-primary);
}

.employeeInfo {
color: var(--stroke-tertiary, #5e5e5e);
Expand All @@ -73,14 +71,10 @@
}

.employeeName {
color: var(--text-primary, #222424);
color: currentColor;
}

.employeeRole {
display: flex;
flex-direction: row;
overflow: visible;
align-self: stretch;
}

.employeeRoleDot::after {
Expand All @@ -101,17 +95,20 @@
color: var(--text-tertiary, #5e5e5e);
}

/* Update the skeleton styles */
/* Skeleton styles */
.skeletonCard {
animation: pulse 1.5s cubic-bezier(0.4, 0, 0.6, 1) infinite;
}

.skeletonText {
height: 20px;
background-color: var(--background-bg-light-primary);
background-color: var(--background-bg-dark);
border-radius: 4px;
align-self: flex-start;
}
.skeletonText--dark {
background-color: var(--background-bg-light-primary);
}

.skeletonName {
width: 150px;
Expand All @@ -134,3 +131,27 @@
opacity: 0.5;
}
}

@container employee (min-width: 380px) {
.employeeWrapper__inner {
flex-direction: row;
}
.employeeEmail {
margin-top: auto;
}
.employeeEmail,
.employeePhone,
.employeeRoleDot {
font-size: 1rem;
}
.employeePhone {
margin-top: 0.5rem;
}
.employeeInfoWrapper {
padding: 0.5rem 0;
}

.skeletonAutoMargin {
margin-top: auto;
}
}
87 changes: 87 additions & 0 deletions src/components/sections/contact-box/ContactBox.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { Suspense } from "react";

import { EmployeeCardSkeleton } from "src/components/employeeCard/EmployeeCard";
import Text from "src/components/text/Text";
import { ChewbaccaEmployee } from "src/types/employees";
import { fetchEmployeesByEmails } from "src/utils/employees";
import { ContactBoxSection } from "studio/lib/interfaces/pages";
import { EMPLOYEE_PAGE_SLUG_QUERY } from "studio/lib/queries/siteSettings";
import { loadStudioQuery } from "studio/lib/store";

import styles from "./contact-box.module.css";
import ContactSelector, { EmployeeAndTag } from "./ContactSelector";

export interface ContactBoxProps {
section: ContactBoxSection;
language: string;
}

export default async function ContactBox({
section,
language,
}: ContactBoxProps) {
const employeesPageRes = await loadStudioQuery<{ slug: string }>(
EMPLOYEE_PAGE_SLUG_QUERY,
{
language,
},
);
const employeesPageSlug = employeesPageRes.data.slug;

const contactPoints = fetchEmployeesByEmails(
section.contactPoints.map((contactPoint) => contactPoint.email),
).then((result) =>
result.ok
? result.value.map((e) => employeeAndTag(e, section.contactPoints))
: [],
);

const backgroundClass =
section.background === "light" ? styles["contactBox__inner--light"] : "";

return (
<section className={styles.contactBox}>
<div className={`${styles.contactBox__inner} ${backgroundClass}`}>
<div className={styles.textContent}>
<Text type="h3" as="h2">
{section.basicTitle}
</Text>

{section.optionalSubtitle && (
<Text type="bodyBig">{section.optionalSubtitle}</Text>
)}
</div>

<div className={styles.contactSelectorWrapper}>
<Suspense
fallback={<EmployeeCardSkeleton background={section.background} />}
>
<ContactSelector
employeesPageSlug={employeesPageSlug}
contactPoints={contactPoints}
language={language}
background={section.background}
/>
</Suspense>
</div>
</div>
</section>
);
}

function employeeAndTag(
employee: ChewbaccaEmployee,
contactPoints: ContactBoxSection["contactPoints"],
): EmployeeAndTag {
const tag =
contactPoints.find((contactPoint) => contactPoint.email === employee.email)
?.tag ?? "";
return {
employee,
tag,
tagSlug: slugify(tag),
};
}
function slugify(tag: string) {
return tag.toLowerCase().replace(/ /g, "-");
}
Loading

0 comments on commit 513b262

Please sign in to comment.