From f6751f509d2535a3012643a1e7bcdb5a7625d639 Mon Sep 17 00:00:00 2001 From: Michael Dinerstein Date: Wed, 30 Oct 2024 13:39:08 -0700 Subject: [PATCH] feat: support project-specific skills --- sanity.types.ts | 48 ++-- schema.json | 216 ++++++++++-------- .../sections/WorkExperience/Projects.tsx | 102 ++++++--- src/graphql/getPositions.ts | 10 +- src/graphql/getSkills.ts | 4 +- src/sanity/schemaTypes/index.ts | 4 +- src/sanity/schemaTypes/projectSkillType.ts | 29 +++ src/sanity/schemaTypes/projectType.ts | 2 +- 8 files changed, 268 insertions(+), 147 deletions(-) create mode 100644 src/sanity/schemaTypes/projectSkillType.ts diff --git a/sanity.types.ts b/sanity.types.ts index d9ee716..2e61368 100644 --- a/sanity.types.ts +++ b/sanity.types.ts @@ -119,17 +119,15 @@ export type Education = { dateAwarded?: string; }; -export type Skill = { - _id: string; - _type: "skill"; - _createdAt: string; - _updatedAt: string; - _rev: string; - title?: string; - icon?: string; - yearStart?: string; - totalYears?: number; - description?: BlockContent; +export type ProjectSkill = { + _type: "projectSkill"; + skill?: { + _ref: string; + _type: "reference"; + _weak?: boolean; + [internalGroqTypeReferenceTo]?: "skill"; + }; + projectSkillDescription?: BlockContent; }; export type Project = { @@ -139,13 +137,11 @@ export type Project = { _updatedAt: string; _rev: string; title?: string; - skills?: Array<{ - _ref: string; - _type: "reference"; - _weak?: boolean; - _key: string; - [internalGroqTypeReferenceTo]?: "skill"; - }>; + skills?: Array< + { + _key: string; + } & ProjectSkill + >; body?: BlockContent; }; @@ -191,6 +187,19 @@ export type Company = { }>; }; +export type Skill = { + _id: string; + _type: "skill"; + _createdAt: string; + _updatedAt: string; + _rev: string; + title?: string; + icon?: string; + yearStart?: string; + totalYears?: number; + description?: BlockContent; +}; + export type ThemeOptions = { _id: string; _type: "themeOptions"; @@ -282,10 +291,11 @@ export type AllSanitySchemaTypes = | Slug | BlockContent | Education - | Skill + | ProjectSkill | Project | Position | Company + | Skill | ThemeOptions | SanityImageCrop | SanityImageHotspot diff --git a/schema.json b/schema.json index 5a8021e..8bb3e95 100644 --- a/schema.json +++ b/schema.json @@ -662,75 +662,56 @@ } }, { - "name": "skill", - "type": "document", - "attributes": { - "_id": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_type": { - "type": "objectAttribute", - "value": { - "type": "string", - "value": "skill" - } - }, - "_createdAt": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_updatedAt": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "_rev": { - "type": "objectAttribute", - "value": { - "type": "string" - } - }, - "title": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "icon": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "yearStart": { - "type": "objectAttribute", - "value": { - "type": "string" - }, - "optional": true - }, - "totalYears": { - "type": "objectAttribute", - "value": { - "type": "number" + "name": "projectSkill", + "type": "type", + "value": { + "type": "object", + "attributes": { + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "projectSkill" + } }, - "optional": true - }, - "description": { - "type": "objectAttribute", - "value": { - "type": "inline", - "name": "blockContent" + "skill": { + "type": "objectAttribute", + "value": { + "type": "object", + "attributes": { + "_ref": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "reference" + } + }, + "_weak": { + "type": "objectAttribute", + "value": { + "type": "boolean" + }, + "optional": true + } + }, + "dereferencesTo": "skill" + }, + "optional": true }, - "optional": true + "projectSkillDescription": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "blockContent" + }, + "optional": true + } } } }, @@ -783,38 +764,16 @@ "of": { "type": "object", "attributes": { - "_ref": { + "_key": { "type": "objectAttribute", "value": { "type": "string" } - }, - "_type": { - "type": "objectAttribute", - "value": { - "type": "string", - "value": "reference" - } - }, - "_weak": { - "type": "objectAttribute", - "value": { - "type": "boolean" - }, - "optional": true } }, - "dereferencesTo": "skill", "rest": { - "type": "object", - "attributes": { - "_key": { - "type": "objectAttribute", - "value": { - "type": "string" - } - } - } + "type": "inline", + "name": "projectSkill" } } }, @@ -1064,6 +1023,79 @@ } } }, + { + "name": "skill", + "type": "document", + "attributes": { + "_id": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_type": { + "type": "objectAttribute", + "value": { + "type": "string", + "value": "skill" + } + }, + "_createdAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_updatedAt": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "_rev": { + "type": "objectAttribute", + "value": { + "type": "string" + } + }, + "title": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "icon": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "yearStart": { + "type": "objectAttribute", + "value": { + "type": "string" + }, + "optional": true + }, + "totalYears": { + "type": "objectAttribute", + "value": { + "type": "number" + }, + "optional": true + }, + "description": { + "type": "objectAttribute", + "value": { + "type": "inline", + "name": "blockContent" + }, + "optional": true + } + } + }, { "name": "themeOptions", "type": "document", diff --git a/src/components/sections/WorkExperience/Projects.tsx b/src/components/sections/WorkExperience/Projects.tsx index 16920ea..270e0fe 100644 --- a/src/components/sections/WorkExperience/Projects.tsx +++ b/src/components/sections/WorkExperience/Projects.tsx @@ -1,4 +1,5 @@ import { Accordion, AccordionDetails, AccordionSummary } from "@mui/material"; +import { SyntheticEvent, useContext, useState } from "react"; import { ChevronDownIcon } from "@sanity/icons"; import { DataContext } from "@/context/DataContext"; @@ -7,7 +8,6 @@ import { ProjectWithRefs } from "@/graphql/getPositions"; import { SkillItem } from "@/components/sections/Skills/SkillItem"; import { SkillWithDescriptionRaw } from "@/graphql/getSkills"; import styles from "./Projects.module.scss"; -import { useContext } from "react"; /** * This contains the top-level project description and skill tags. It can @@ -30,6 +30,57 @@ const ProjectItem = ({ ); +const ProjectAccordion = ({ + project, + projectSkills, +}: { + project: ProjectWithRefs; + projectSkills: SkillWithDescriptionRaw[]; +}) => { + const [expanded, setExpanded] = useState(false); + + const handleAccordionChange = (event: SyntheticEvent, isExpanded: boolean) => { + const target = event.target as HTMLElement; + const isButtonClick = target.closest(".MuiButton-root"); + const isDialogClick = target.closest(".MuiDialog-root"); + + if (isButtonClick || isDialogClick) { + setExpanded(true); // Ensure the accordion remains open. + return; + } + + setExpanded(isExpanded); + }; + + return ( + + } + > + + + ({ + backgroundColor: theme.palette.primary.light, + padding: "2rem", + })} + className={styles.accordionDetails} + > + + + + ); +}; + /** * This is the primary Projects component, which renders as list of all projects. * These can be contained in an Accordion component, or as a standalone list item. @@ -38,48 +89,37 @@ export const Projects = ({ projects }: { projects: ProjectWithRefs[] }) => { const { skills } = useContext(DataContext); return projects.map((project) => { - // Get all the skills used in this project. - const projectSkills = project?.skills + const projectSkills = project?.skills?.length ? skills.reduce((acc, skill) => { - // Check if the skill is linked to the project - if (skill?.title && project.skills.map((s) => s._id).includes(skill._id)) { - acc.push(skill); + const skillClone = { ...skill }; // Ensure we're not affecting the original properties. + + const skillIsInProject = + skillClone?.title && project.skills.map((s) => s.skill._id).includes(skillClone._id); + + if (!skillIsInProject) return acc; + + // It's linked! Ensure we include the project-specific description. + const matchedSkill = project.skills.find((s) => s.skill._id === skillClone._id); + if (matchedSkill && matchedSkill?.projectSkillDescriptionRaw) { + skillClone.descriptionRaw = matchedSkill.projectSkillDescriptionRaw; } + acc.push(skillClone); + return acc; }, []) : []; // If the project contains a block description, we'll // use an Accordion component to render it. - if (project?.bodyRaw) { + if (project?.bodyRaw) return ( - - } - > - - - ({ - backgroundColor: theme.palette.primary.light, - padding: "2rem", - })} - className={styles.accordionDetails} - > - - - + project={project} + projectSkills={projectSkills} + /> ); - } // Otherwise, show the project overview and skills only. return ( diff --git a/src/graphql/getPositions.ts b/src/graphql/getPositions.ts index f5270c4..f48ecbe 100644 --- a/src/graphql/getPositions.ts +++ b/src/graphql/getPositions.ts @@ -19,7 +19,10 @@ export const GET_POSITIONS = gql` title bodyRaw skills { - _id + skill { + _id + } + projectSkillDescriptionRaw } } } @@ -27,7 +30,10 @@ export const GET_POSITIONS = gql` `; export type ProjectWithRefs = Omit & { - skills: SkillWithDescriptionRaw[]; + skills: { + skill: SkillWithDescriptionRaw; + projectSkillDescriptionRaw: BlockContent; + }[]; bodyRaw: BlockContent; }; diff --git a/src/graphql/getSkills.ts b/src/graphql/getSkills.ts index 8731725..742d461 100644 --- a/src/graphql/getSkills.ts +++ b/src/graphql/getSkills.ts @@ -15,7 +15,9 @@ export const GET_SKILLS = gql` } `; -export type SkillWithDescriptionRaw = Omit & { descriptionRaw: BlockContent }; +export type SkillWithDescriptionRaw = Skill & { + descriptionRaw: BlockContent; +}; export type AllSkill = { allSkill: SkillWithDescriptionRaw[]; diff --git a/src/sanity/schemaTypes/index.ts b/src/sanity/schemaTypes/index.ts index 4edcdab..8a49f95 100644 --- a/src/sanity/schemaTypes/index.ts +++ b/src/sanity/schemaTypes/index.ts @@ -5,16 +5,18 @@ import { skillType } from "./skillType"; import { companyType } from "./companyType"; import { positionType } from "./positionType"; import { projectType } from "./projectType"; +import { projectSkillType } from "./projectSkillType"; import { educationType } from "./educationType"; import { blockContentType } from "./blockContentType"; export const schema: { types: SchemaTypeDefinition[] } = { types: [ themeOptionsType, + skillType, companyType, positionType, projectType, - skillType, + projectSkillType, educationType, blockContentType, ], diff --git a/src/sanity/schemaTypes/projectSkillType.ts b/src/sanity/schemaTypes/projectSkillType.ts new file mode 100644 index 0000000..01a39c8 --- /dev/null +++ b/src/sanity/schemaTypes/projectSkillType.ts @@ -0,0 +1,29 @@ +import { defineField, defineType } from "sanity"; + +export const projectSkillType = defineType({ + name: "projectSkill", + title: "Project Skill", + type: "object", + fields: [ + defineField({ + name: "skill", + type: "reference", + to: { type: "skill" }, + }), + defineField({ + name: "projectSkillDescription", + type: "blockContent", + }), + ], + preview: { + select: { + title: "skill.title", + }, + prepare(selection) { + const { title } = selection; + return { + title: title || "(No skill name)", + }; + }, + }, +}); diff --git a/src/sanity/schemaTypes/projectType.ts b/src/sanity/schemaTypes/projectType.ts index 661cc5b..5cacbe1 100644 --- a/src/sanity/schemaTypes/projectType.ts +++ b/src/sanity/schemaTypes/projectType.ts @@ -15,7 +15,7 @@ export const projectType = defineType({ defineField({ name: "skills", type: "array", - of: [defineArrayMember({ type: "reference", to: { type: "skill" } })], + of: [defineArrayMember({ type: "projectSkill" })], }), defineField({ name: "body",