From 474e28004d3065e16a317dacb53f2aa541ab356c Mon Sep 17 00:00:00 2001 From: Mikael Brevik Date: Fri, 29 Nov 2024 09:01:04 +0100 Subject: [PATCH] fix: polishing employee page (#919) * fix: makes loading employees lazy and more performant * fix: people grid as actual grid * fixing the most annoying thing I can think of * feat: adds tag component * fix: adjustments to minor details of design * fix: makes employee site more mobile friendly * woop * fix: adds max width for employee container --- src/components/employeeCard/EmployeeCard.tsx | 64 +++++++---- .../employeeCard/employeeCard.module.css | 45 ++++++-- .../sections/employees/EmployeeList.tsx | 104 +++++++++--------- .../sections/employees/Employees.tsx | 48 ++++---- .../sections/employees/employees.module.css | 31 ++++-- src/components/tag/index.tsx | 37 +++++++ src/components/tag/tag.module.css | 26 +++++ src/components/text/Text.tsx | 4 +- src/utils/employees.ts | 11 +- 9 files changed, 251 insertions(+), 119 deletions(-) create mode 100644 src/components/tag/index.tsx create mode 100644 src/components/tag/tag.module.css diff --git a/src/components/employeeCard/EmployeeCard.tsx b/src/components/employeeCard/EmployeeCard.tsx index 0f65ece5d..f4d74ac36 100644 --- a/src/components/employeeCard/EmployeeCard.tsx +++ b/src/components/employeeCard/EmployeeCard.tsx @@ -30,38 +30,58 @@ export default function EmployeeCard({
-
-

{employee.name}

-
- {employee.competences.map((competence) => ( - <> - - {competence} - - - ))} -
-
+ + + {employee.name} + + -
-

{employee.email}

- {employee.telephone && ( -

{formatPhoneNumber(employee.telephone)}

- )} +
+ {employee.competences.map((competence) => ( + + {competence} + + ))}
+ + + {employee.email} + + {employee.telephone && ( + + + {formatPhoneNumber(employee.telephone)} + + + )}
) ); } + +export function EmployeeCardSkeleton() { + return ( +
+
+
+
+
+
+
+ ); +} diff --git a/src/components/employeeCard/employeeCard.module.css b/src/components/employeeCard/employeeCard.module.css index a49534326..c99bd84b2 100644 --- a/src/components/employeeCard/employeeCard.module.css +++ b/src/components/employeeCard/employeeCard.module.css @@ -20,7 +20,7 @@ display: flex; flex-direction: column; justify-content: space-between; - height: 100%; + gap: 0.375rem; } .contactInfo { @@ -30,17 +30,14 @@ gap: 0.375rem; } -.employeeName { - font-size: 1rem; - font-weight: 600; - line-height: 120%; +.employeeNameLink { + text-decoration: none; } .employeeWrapper { display: flex; flex-direction: column; align-items: flex-start; - margin: 1rem; min-width: 280px; max-width: var(--Text-paragraph, 537px); gap: var(--small, 6px); @@ -88,7 +85,7 @@ .employeeRoleDot::after { content: "ยท"; - margin: 0 0.5rem; + margin: 0 0.25rem; } .employeeRoleDot:last-child:after { @@ -103,3 +100,37 @@ font-size: 1rem; color: var(--text-tertiary, #5e5e5e); } + +/* Update the 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); + border-radius: 4px; + align-self: flex-start; +} + +.skeletonName { + width: 150px; +} + +.skeletonTitle { + width: 100px; +} + +.skeletonContact { + width: 130px; +} + +@keyframes pulse { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.5; + } +} diff --git a/src/components/sections/employees/EmployeeList.tsx b/src/components/sections/employees/EmployeeList.tsx index a6cd88c18..bb041e4cc 100644 --- a/src/components/sections/employees/EmployeeList.tsx +++ b/src/components/sections/employees/EmployeeList.tsx @@ -1,12 +1,13 @@ "use client"; import { useTranslations } from "next-intl"; -import { useState } from "react"; +import { use, useState } from "react"; -import Button from "src/components/buttons/Button"; import EmployeeCard from "src/components/employeeCard/EmployeeCard"; +import { Tag } from "src/components/tag"; import Text from "src/components/text/Text"; import { ChewbaccaEmployee, Competence } from "src/types/employees"; +import { Result } from "studio/utils/result"; import styles from "./employees.module.css"; @@ -18,7 +19,7 @@ const competences: Competence[] = [ ]; export interface EmployeesProps { - employees: ChewbaccaEmployee[]; + employees: Promise>; language: string; employeesPageSlug: string; } @@ -29,16 +30,19 @@ interface EmployeeFilters { } export default function EmployeeList({ - employees, + employees: employeesPromise, language, employeesPageSlug, }: EmployeesProps) { + const employeesRes = use(employeesPromise); + const employees = employeesRes.ok ? employeesRes.value : []; + const [filteredEmployees, setFilteredEmployees] = useState< + ChewbaccaEmployee[] + >(shuffleEmployees(employees)); + const locations = Array.from(new Set(employees.map((e) => e.officeName))); const t = useTranslations("employee_card"); - const [filteredEmployees, setFilteredEmployees] = - useState(employees); - const [employeeFilters, setEmployeeFilters] = useState({ competenceFilter: null, locationFilter: null, @@ -85,30 +89,25 @@ export default function EmployeeList({ {t("field")} - + text={t("all")} + /> + {competences.map((competence) => { const active = employeeFilters.competenceFilter == competence; return ( - + text={t(competence)} + /> ); })}
@@ -116,58 +115,53 @@ export default function EmployeeList({ {t("location")} - + text={t("all")} + /> {locations.map((location) => { + if (!location) return null; const active = employeeFilters.locationFilter == location; return ( - + text={location} + /> ); })}
-
-
- +

{t("show")}{" "} {filteredEmployees.length} {" "} - {t("of")}{" "} - {employees.length}{" "} - {t("consultants")} + {t("of")} {employees.length} {t("consultants")}

-
-
- {filteredEmployees.map((employee) => ( - - ))} +
+ {filteredEmployees.map((employee) => ( + + ))} +
); } + +function shuffleEmployees(employees: ChewbaccaEmployee[]) { + return employees.sort(() => Math.random() - 0.5); +} diff --git a/src/components/sections/employees/Employees.tsx b/src/components/sections/employees/Employees.tsx index ef1d07748..1f0c51426 100644 --- a/src/components/sections/employees/Employees.tsx +++ b/src/components/sections/employees/Employees.tsx @@ -1,9 +1,9 @@ import { headers } from "next/headers"; +import { Suspense } from "react"; -import { - domainFromEmail, - fetchAllChewbaccaEmployees, -} from "src/utils/employees"; +import { EmployeeCardSkeleton } from "src/components/employeeCard/EmployeeCard"; +import Text from "src/components/text/Text"; +import { fetchAllChewbaccaEmployees } from "src/utils/employees"; import { domainFromHostname } from "src/utils/url"; import { EmployeesSection } from "studio/lib/interfaces/pages"; import { EMPLOYEE_PAGE_SLUG_QUERY } from "studio/lib/queries/siteSettings"; @@ -11,6 +11,7 @@ import { loadStudioQuery } from "studio/lib/store"; import EmployeeList from "./EmployeeList"; import styles from "./employees.module.css"; + export interface EmployeesProps { language: string; section: EmployeesSection; @@ -25,29 +26,32 @@ export default async function Employees({ language, section }: EmployeesProps) { ); const employeesPageSlug = employeesPageRes.data.slug; - const employeesResult = await fetchAllChewbaccaEmployees(); - - if (!employeesResult.ok) { - console.error("Failed to fetch employees: ", employeesResult.error); - return; - } - - const domain = domainFromHostname(headers().get("host")); - const employees = employeesResult.value.filter( - (employee) => - employee.email != null && domainFromEmail(employee.email) === domain, - ); + const countryTld = domainFromHostname(headers().get("host")).split(".")[1]; + const employees = fetchAllChewbaccaEmployees(countryTld); return (
-

{section.basicTitle}

- + {section.basicTitle} + + }> + +
); } + +function EmployeeListSkeleton() { + return ( +
+ {[...Array(4)].map((_, index) => ( + + ))} +
+ ); +} diff --git a/src/components/sections/employees/employees.module.css b/src/components/sections/employees/employees.module.css index e51a5801b..01a0e1ebf 100644 --- a/src/components/sections/employees/employees.module.css +++ b/src/components/sections/employees/employees.module.css @@ -3,8 +3,19 @@ display: flex; flex-direction: column; align-items: center; - margin: 5rem 0; + margin: 5rem 1rem; flex-wrap: wrap; + + max-width: var(--max-content-width-large); + margin: 0 auto; + + background: var(--background-bg-light-secondary); + padding: 3rem; + border-radius: var(--radius-large, 24px); + + @media (max-width: 375px) { + padding: 1.5rem; + } } .header { @@ -42,12 +53,16 @@ .employeeFilterLabel { flex-shrink: 0; - min-width: 4.5rem; - width: 4.5rem; + @media (min-width: 375px) { + min-width: 4.5rem; + width: 4.5rem; + } } -.employeeCountWrapper { - width: 100%; +.peopleCountWrapper { + display: flex; + flex-direction: column; + gap: 0.5rem; } .employeeCount { @@ -59,7 +74,7 @@ } .peopleContainer { - display: flex; - flex-wrap: wrap; - gap: 1rem; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); + gap: 1.5rem; } diff --git a/src/components/tag/index.tsx b/src/components/tag/index.tsx new file mode 100644 index 000000000..18f16130e --- /dev/null +++ b/src/components/tag/index.tsx @@ -0,0 +1,37 @@ +import Link from "next/link"; + +import Text from "src/components/text/Text"; + +import styles from "./tag.module.css"; + +type TagInner = + | { + type: "button"; + onClick?: () => void; + } + | { + type: "link"; + href: string; + }; + +type TagProps = { + active?: boolean; + text: string; +} & TagInner; + +export const Tag = ({ text, active = false, ...props }: TagProps) => { + const className = `${styles.tag} ${active ? styles["tag--active"] : ""}`; + if (props.type === "button") { + return ( + + ); + } + + return ( + + {text} + + ); +}; diff --git a/src/components/tag/tag.module.css b/src/components/tag/tag.module.css new file mode 100644 index 000000000..bb02f5306 --- /dev/null +++ b/src/components/tag/tag.module.css @@ -0,0 +1,26 @@ +.tag { + display: flex; + align-items: center; + gap: 0.5rem; + padding: 0.375rem 0.75rem; + border: 1px solid var(--text-primary, #222424); + background: transparent; + border-radius: 9999px; +} + +.tag:hover { + background: var(--background-bg-light-secondary, #eaeaea); +} +.tag:active { + background: var(--background-bg-light-secondary, #eaeaea); +} +.tag:focus { + outline: 2px solid var(--surface-yellow, #ffd02f); +} +.tag--active { + background: var(--background-bg-dark, #2d2d2d); + color: var(--text-primary-light); +} +.tag--active:hover { + background: var(--background-bg-dark, #2d2d2d); +} diff --git a/src/components/text/Text.tsx b/src/components/text/Text.tsx index cb8c83341..ce7ad46e3 100644 --- a/src/components/text/Text.tsx +++ b/src/components/text/Text.tsx @@ -79,13 +79,15 @@ const Text = ({ children, id, className, + as: asElement, }: { type?: TextType; children: React.ReactNode; id?: string; + as?: React.ElementType; className?: string; }) => { - const Element = elementMap[type]; + const Element = asElement ?? elementMap[type]; const generatedClassName = `${classMap[type]} ${className ?? ""}`; return ( diff --git a/src/utils/employees.ts b/src/utils/employees.ts index 1e0720140..38bdf5e25 100644 --- a/src/utils/employees.ts +++ b/src/utils/employees.ts @@ -8,10 +8,13 @@ import { domainFromHostname } from "./url"; const CHEWBACCA_URL = "https://chewie-webapp-ld2ijhpvmb34c.azurewebsites.net"; -export async function fetchAllChewbaccaEmployees(): Promise< - Result -> { - const employeesRes = await fetch(new URL("employees", CHEWBACCA_URL)); +export async function fetchAllChewbaccaEmployees( + countryCode?: string, +): Promise> { + const url = countryCode + ? `${CHEWBACCA_URL}/employees?country=${countryCode}` + : `${CHEWBACCA_URL}/employees`; + const employeesRes = await fetch(url); if (!employeesRes.ok) { return ResultError( `Fetch returned status ${employeesRes.status} ${employeesRes.statusText}`,