Skip to content

Commit

Permalink
✨ Show Jobs section with job postings (#917)
Browse files Browse the repository at this point in the history
* ✨ Add section for jobs
Update query for fetching job postings

* 💄 Add Jobs section

* ♻️ Add IJobPostings interface

* 📱 Make it responsive!

* 👌 Use rem rather than px
♻️ Use &:: in .jobPosting

* ✨ Add location filter for job postings

* ♻️ jobPostingsFilterWrapper => filters

* 💄 Display job locations as Badges

* 💄 Add violet background to Tag component
  • Loading branch information
petterhh authored Dec 2, 2024
1 parent fe6fc55 commit 184d620
Show file tree
Hide file tree
Showing 15 changed files with 354 additions and 6 deletions.
31 changes: 31 additions & 0 deletions src/components/jobPosting/JobPosting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import Badge from "src/components/badge/Badge";
import Text from "src/components/text/Text";
import { CompanyLocation } from "studio/lib/interfaces/companyDetails";
import { IJobPosting } from "studio/lib/interfaces/jobPosting";

import styles from "./jobPosting.module.css";

interface JobPostingProps {
jobPosting: IJobPosting;
}

export default function JobPosting({ jobPosting }: JobPostingProps) {
const jobPostingLocations = jobPosting.locations
.map((location: CompanyLocation) => location.companyLocationName)
.join(", ");

return (
<a
href={jobPosting.recruiteeAdUrl}
target="_blank"
className={styles.jobPosting}
>
<div className={styles.details}>
<div className={styles.role}>
<Text type={"h5"}>{jobPosting.role}</Text>
</div>
<Badge>{jobPostingLocations}</Badge>
</div>
</a>
);
}
52 changes: 52 additions & 0 deletions src/components/jobPosting/jobPosting.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
.jobPosting {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
border-radius: 1.5rem;
padding: 0.5rem 1.5rem;
gap: 5rem;
width: 100%;
background-color: var(--text-primary-light);
color: var(--text-primary);

&::after {
justify-self: end;
width: 1.5rem;
height: 1.5rem;
content: "";
-webkit-mask: url("/_assets/arrow-right.svg") no-repeat center;
-webkit-mask-size: cover;
mask: url("/_assets/arrow-right.svg") no-repeat center;
mask-size: cover;
background-color: var(--background-bg-dark);
}
}

.details {
display: flex;
flex-direction: row;
justify-content: flex-start;
align-items: center;
gap: 0.5rem;
width: 100%;
}

.role {
width: 250px;
@media (max-width: 640px) {
width: 100%;
}
}

.locations {
border: solid;
border-color: var(--text-primary);
border-width: thin;
border-radius: 25rem;
padding: 0.375rem 0.75rem;
text-align: center;
@media (max-width: 640px) {
display: none;
}
}
81 changes: 81 additions & 0 deletions src/components/sections/jobs/JobPostingList.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
"use client";

import { useEffect, useMemo, useState } from "react";

import JobPosting from "src/components/jobPosting/JobPosting";
import { Tag } from "src/components/tag";
import Text from "src/components/text/Text";
import { CompanyLocation } from "studio/lib/interfaces/companyDetails";
import { IJobPosting } from "studio/lib/interfaces/jobPosting";

import styles from "./jobs.module.css";

interface JobPostingListProps {
jobPostings: IJobPosting[];
companyLocations: CompanyLocation[];
}

export default function JobPostingList({
jobPostings,
companyLocations,
}: JobPostingListProps) {
const [locationFilter, setLocationFilter] = useState<CompanyLocation | null>(
null,
);
const [filteredJobPostings, setFilteredJobPostings] =
useState<IJobPosting[]>(jobPostings);
const jobPostingLocations = useMemo(
() =>
Object.fromEntries(
jobPostings.map((jobPosting) => [
jobPosting._key,
jobPosting.locations.map((location) => location.companyLocationName),
]),
),
[jobPostings],
);

useEffect(() => {
if (locationFilter === null) {
setFilteredJobPostings(jobPostings);
} else {
setFilteredJobPostings(
jobPostings?.filter((jobPosting) =>
jobPostingLocations[jobPosting._key].includes(
locationFilter.companyLocationName,
),
),
);
}
}, [locationFilter, jobPostings, jobPostingLocations]);

return (
<div className={styles.jobPostingsContainer}>
<div className={styles.filters}>
<Text type="labelRegular">Kontor</Text>
<Tag
active={!locationFilter}
type="button"
onClick={() => setLocationFilter(null)}
text="Alle"
background="violet"
/>
{companyLocations.map((location: CompanyLocation) => (
<Tag
active={locationFilter === location}
type="button"
onClick={() => setLocationFilter(location)}
text={location.companyLocationName}
key={location._id}
background="violet"
/>
))}
</div>
<div className={styles.jobPostings}>
{filteredJobPostings?.map((jobPosting: IJobPosting) => (
<JobPosting jobPosting={jobPosting} key={jobPosting._key} />
))}
</div>
</div>
);
}
47 changes: 47 additions & 0 deletions src/components/sections/jobs/Jobs.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import Text from "src/components/text/Text";
import { CompanyLocation } from "studio/lib/interfaces/companyDetails";
import { IJobPostings } from "studio/lib/interfaces/jobPosting";
import { JobsSection } from "studio/lib/interfaces/pages";
import {
COMPANY_LOCATIONS_QUERY,
JOB_POSTINGS_QUERY,
} from "studio/lib/queries/admin";
import { loadStudioQuery } from "studio/lib/store";

import JobPostingList from "./JobPostingList";
import styles from "./jobs.module.css";

export interface JobsProps {
language: string;
section: JobsSection;
}

export default async function Jobs({ language, section }: JobsProps) {
const { data: jobPostings } = await loadStudioQuery<IJobPostings | null>(
JOB_POSTINGS_QUERY,
{
language,
},
);

const { data: companyLocations } = await loadStudioQuery<CompanyLocation[]>(
COMPANY_LOCATIONS_QUERY,
{},
);

return (
jobPostings &&
companyLocations && (
<div className={styles.wrapper}>
<div className={styles.titleSection}>
<Text type={"h2"}>{section.basicTitle}</Text>
<Text type={"bodyNormal"}>{section.subtitle}</Text>
</div>
<JobPostingList
jobPostings={jobPostings.jobPostingsArray}
companyLocations={companyLocations}
/>
</div>
)
);
}
39 changes: 39 additions & 0 deletions src/components/sections/jobs/jobs.module.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
.wrapper {
display: flex;
flex-direction: column;
justify-content: center;
padding: 2rem 0.375rem 0.375rem 0.375rem;
max-width: 53rem;
margin: 5rem auto;
border-radius: 0.75px;
gap: 1.5rem;
background-color: var(--Violet-700);
color: var(--text-primary-light);
}

.titleSection {
padding: 0px 24px;
}

.jobPostingsContainer {
display: flex;
flex-direction: column;
gap: 1rem;
}

.filters {
display: flex;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
padding: 0rem 1.5rem;
gap: 0.75rem;
}

.jobPostings {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.5rem;
text-wrap: wrap;
}
19 changes: 16 additions & 3 deletions src/components/tag/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,22 @@ type TagInner =
href: string;
} & JSX.IntrinsicElements["link"]);

function getBackgroundClass(backgroundColor: string) {
switch (backgroundColor) {
case "light":
return "";
case "dark":
return styles["tag--bgDark"];
case "violet":
return styles["tag--bgViolet"];
default:
return "";
}
}

type TagProps = {
active?: boolean;
background?: "light" | "dark";
background?: "light" | "dark" | "violet";
text: string;
} & TagInner;

Expand All @@ -27,8 +40,8 @@ export const Tag = ({
...props
}: TagProps) => {
const activeClass = active ? styles["tag--active"] : "";
const bgDarkClass = background === "dark" ? styles["tag--bgDark"] : "";
const className = `${styles.tag} ${activeClass} ${bgDarkClass}`;
const bgClass = getBackgroundClass(background);
const className = `${styles.tag} ${activeClass} ${bgClass}`;
if (props.type === "button") {
return (
<button className={className} onClick={props.onClick}>
Expand Down
17 changes: 17 additions & 0 deletions src/components/tag/tag.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,19 @@
--_tag-background: transparent;
}

.tag--bgViolet {
--_tag-borderColor: var(--text-primary-light);
--_tag-color: var(--text-primary-light);
}
.tag--bgViolet:active {
--_tag-background: var(--text-primary-light);
--_tag-color: var(--text-primary);
}
.tag--bgViolet:hover {
--_tag-color: var(--text-primary-light);
--_tag-background: transparent;
}

.tag--active {
--_tag-background: var(--background-bg-dark);
--_tag-color: var(--text-primary-light);
Expand All @@ -49,3 +62,7 @@
--_tag-background: var(--background-bg-light-primary);
--_tag-color: var(--text-primary);
}
.tag--active.tag--bgViolet {
--_tag-background: var(--text-primary-light);
--_tag-color: var(--text-primary);
}
1 change: 1 addition & 0 deletions src/styles/global.css
Original file line number Diff line number Diff line change
Expand Up @@ -155,4 +155,5 @@ small {

a {
color: var(--text-primary);
text-decoration: none;
}
3 changes: 3 additions & 0 deletions src/utils/renderSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ImageSplitComponent from "src/components/sections/image-split/ImageSplit"
import ImageSplitComponentPreview from "src/components/sections/image-split/ImageSplitPreview";
import ImageSectionComponent from "src/components/sections/imageSection/ImageSectionComponent";
import ImageSectionComponentPreview from "src/components/sections/imageSection/ImageSectionComponentPreview";
import Jobs from "src/components/sections/jobs/Jobs";
import { LogoSalad } from "src/components/sections/logoSalad/LogoSalad";
import LogoSaladPreview from "src/components/sections/logoSalad/LogoSaladPreview";
import { Testimonials } from "src/components/sections/testimonials/Testimonials";
Expand Down Expand Up @@ -255,6 +256,8 @@ const SectionRenderer = ({
return <ContactBox section={section} language={language} />;
case "employees":
return <Employees language={language} section={section} />;
case "jobs":
return <Jobs language={language} section={section} />;
default:
return null;
}
Expand Down
12 changes: 12 additions & 0 deletions studio/lib/interfaces/jobPosting.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { CompanyLocation } from "studio/lib/interfaces/companyDetails";

export interface IJobPosting {
_key: string;
role: string;
locations: CompanyLocation[];
recruiteeAdUrl: string;
}

export interface IJobPostings {
jobPostingsArray: IJobPosting[];
}
10 changes: 9 additions & 1 deletion studio/lib/interfaces/pages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,13 @@ export interface EmployeesSection {
basicTitle: string;
}

export interface JobsSection {
_type: "jobs";
_key: string;
basicTitle: string;
subtitle: string;
}

export type Section =
| HeroSection
| LogoSaladSection
Expand All @@ -120,7 +127,8 @@ export type Section =
| ImageSplitSection
| GridSection
| ContactBoxSection
| EmployeesSection;
| EmployeesSection
| JobsSection;

export interface PageBuilder {
_createdAt: string;
Expand Down
Loading

0 comments on commit 184d620

Please sign in to comment.