From 86a893398a3bbb197d3a9fc0c928394b0a05749c Mon Sep 17 00:00:00 2001 From: Alexander Date: Thu, 20 Feb 2025 13:04:14 +0400 Subject: [PATCH] skills sorting added --- src/cv-maker/core/schema.ts | 27 +++++------ src/cv-maker/core/template.tsx | 45 ++++++++++++------- src/cv-maker/core/utils.ts | 34 +++++++++++--- src/cv-maker/main.ts | 19 +++++--- .../static/{john-doe.json => john-doe.jsonc} | 3 +- src/cv-maker/static/resume-schema.json | 7 +++ 6 files changed, 89 insertions(+), 46 deletions(-) rename src/cv-maker/static/{john-doe.json => john-doe.jsonc} (98%) diff --git a/src/cv-maker/core/schema.ts b/src/cv-maker/core/schema.ts index 84d4bcc..5c680dd 100644 --- a/src/cv-maker/core/schema.ts +++ b/src/cv-maker/core/schema.ts @@ -6,27 +6,22 @@ const httpsRegex = /https:\/\//; // hide social profiles // hide phone -export class ViewSettings - extends S.Class("ProjectTechnology")({ - collapseOld: S.Boolean - }) { } +// export class ViewSettings +// extends S.Class("ProjectTechnology")({ +// collapseOld: S.Boolean +// }) { } + +export const STACK_CATEGORIES = [ + "programming language", "cloud computing", "testing", "framework", "frontend", "database", + "devops tool", "collaboration tool", "other" +] as const; export class ProjectTechnology extends S.Class("ProjectTechnology")({ id: S.NonEmptyString, name: S.NonEmptyString, - category: - S.Literal( - "programming language", - "framework", - "cloud computing", - "database", - "devops tool", - "testing", - "collaboration tool", - "frontend", - "other" - ), + category: S.Literal(...STACK_CATEGORIES), + display: S.Literal("force", "hide").pipe(S.optional), version: S.NonEmptyString.pipe(S.optional), url: S.NonEmptyString.pipe(S.optional) }) { } diff --git a/src/cv-maker/core/template.tsx b/src/cv-maker/core/template.tsx index 1faba4b..6710bdb 100644 --- a/src/cv-maker/core/template.tsx +++ b/src/cv-maker/core/template.tsx @@ -1,5 +1,5 @@ -import { EmploymentRecord, ProjectDetails, ProjectTechnology, ResumeObject } from "#/cv-maker/core/schema"; -import { DateTime, pipe, Array } from "effect"; +import { EmploymentRecord, ProjectDetails, ProjectTechnology, ResumeObject, STACK_CATEGORIES } from "#/cv-maker/core/schema"; +import { DateTime, pipe, Array, Order, HashMap } from "effect"; export function Resume(resume: ResumeObject) { const coverLetter = resume.me.coverLetter; @@ -16,9 +16,9 @@ export function Resume(resume: ResumeObject) {
{coverLetter.content.map(line =>

)}
- + - ): null} + ) : null}
Summary @@ -35,12 +35,12 @@ export function Resume(resume: ResumeObject) {
- {Object.entries(getSkills(resume)).map(([category, group]) => ( + {getSkills(resume).map(([category, group]) => (
{category}: {group.map((t, idx) => ( - {t.technology.name} @@ -95,7 +95,7 @@ function CompanyHeader(company: EmploymentRecord) { function ProjectStack(project: ProjectDetails) { return ( - {project.stack.map(t => + {project.stack.map(t => {t}) } @@ -186,15 +186,15 @@ function EmploymentHistory(resume: ResumeObject) { {getPeriod(er)}
{CompanySubHeader(er)} - {isOld ? + {isOld ?
Roles: {allRoles} -
: +
:
- { er.sortedProjects.map((project, id) => CompanyProject(project, id == er.projects.length - 1)) } + {er.sortedProjects.map((project, id) => CompanyProject(project, id == er.projects.length - 1))}
- } + } ) @@ -232,14 +232,14 @@ function getPeriod(company: EmploymentRecord) { } -function getSkills(resume: ResumeObject) { +function getSkills(resume: ResumeObject): [ string, { code: string, technology: ProjectTechnology }[] ][] { const categories: { code: string, technology: ProjectTechnology }[] | undefined = resume.employmentHistory?.flatMap(e => e.projects.flatMap(p => [...p.stack, ...p.tools].flatMap(t => { const technology = resume.technologies?.find(_ => _.id == t); - if (!technology) return []; + if (!technology || technology.display == "hide") return []; return [{ code: t, category: technology.category, @@ -251,14 +251,25 @@ function getSkills(resume: ResumeObject) { if (!categories) { console.warn("Skill categories not found") - return {}; + return []; }; const grouped = pipe( - categories, + [ + ...categories, + ...resume.technologies + .filter(_ => _.display === "force") + .map(_ => ({ + code: _.id, + category: _.category, + technology: _ + })) + ], Array.dedupeWith((a, b) => a.code == b.code), - Array.groupBy(_ => _.technology.category) + Array.groupBy(_ => _.technology.category), + _ => Object.entries(_), + Array.sortWith(t => STACK_CATEGORIES.findIndex(_ => _ == t[0]), Order.number) ); return grouped; diff --git a/src/cv-maker/core/utils.ts b/src/cv-maker/core/utils.ts index 00d97b9..29b1310 100644 --- a/src/cv-maker/core/utils.ts +++ b/src/cv-maker/core/utils.ts @@ -14,19 +14,24 @@ export const resumeObjectToHTML = } -export const getResumeObject = async () => { - const resume = await fetch("./john-doe.json").then(_ => _.json()); +export const getExampleResume = async () => { + const resume = await fetch("./john-doe.jsonc").then(_ => _.text()).then(_ => parseJSON(_, true)); - delete resume["$schema"] + delete resume["$schema"]; const input = S.decodeUnknownSync(ResumeObject)(resume); return input; } -export const parseJSON = (input: string | undefined) => { +export const parseJSON = (input: string | undefined, sanity = false) => { if (!input) return; try { - return JSON.parse(input); + if (sanity) { + return JSON.parse(removeTrailingCommas(removeJsonComments(input))); + } else { + return JSON.parse(input); + } + } catch (error) {} } @@ -52,3 +57,22 @@ export function setUrlParam(name: string, value: string) { url.searchParams.set(name, value); window.history.replaceState(null, '', url); } + +const commentsRegex = /("(?:\\.|[^"\\])*")|(?:\/\*(?:[\s\S]*?)\*\/)|(\/\/.*)/g + +export function removeJsonComments(input: string): string { + return input.replace( + commentsRegex, + (_match, quotedString) => { + if (quotedString !== undefined) { + return quotedString; + } + return ""; + } + ); +} + +const trailingCommasRegex = /,\s*([\]}])/g; +function removeTrailingCommas(input: string): string { + return input.replace(trailingCommasRegex, '$1'); +} diff --git a/src/cv-maker/main.ts b/src/cv-maker/main.ts index 37f0e3a..482e502 100644 --- a/src/cv-maker/main.ts +++ b/src/cv-maker/main.ts @@ -1,5 +1,5 @@ import Alpine from "alpinejs"; -import { debounce, getResumeObject, getUrlParam, parseJSON, resumeObjectToHTML, setUrlParam } from "./core/utils"; +import { debounce, getExampleResume, getUrlParam, parseJSON, resumeObjectToHTML, setUrlParam } from "./core/utils"; import type resumeSchema from "./static/resume-schema.json" import { makeJsonEditor } from "#/common/editor/make"; import { hasMajorError } from "#/common/editor/text-model"; @@ -68,8 +68,8 @@ async function loadStoredResume() { state.availableResumes = []; if (Object.keys(localStorage).length == 0) { - const resume = await getResumeObject(); - localStorage.setItem("example", JSON.stringify(resume)); + const resume = await getExampleResume(); + localStorage.setItem("example", JSON.stringify(resume, undefined, 2)); } for (const key of Object.keys(localStorage)) { @@ -89,7 +89,7 @@ function selectResume() { console.warn("No resume to load"); return; }; - const resume = parseJSON(resumeJson); + const resume = parseJSON(resumeJson, true); if (!resume) { console.warn("Invalid json of resume"); return; @@ -116,6 +116,8 @@ async function setup() { (ref: string) => editor.monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ validate: true, + allowComments: true, + trailingCommas: "ignore", schemas: [ { uri: "resume.json", @@ -134,14 +136,17 @@ async function setup() { editor.textModel.model.setValue(JSON.stringify(state.resumeObject, undefined, 2)); setupJsonSchema("#/$defs/ResumeObject"); } else { - editor.textModel.model.setValue(JSON.stringify(state.resumeObject[state.editorSection], undefined, 2)) + const section = state.resumeObject[state.editorSection] ?? {}; + editor.textModel.model.setValue(JSON.stringify(section, undefined, 2)) setupJsonSchema(`#/$defs/ResumeObject/properties/${state.editorSection}`); } } const saveResume = () => { - const parsed = parseJSON(editor.textModel.model.getValue()); - if (!parsed) return; + const parsed = parseJSON(editor.textModel.model.getValue(), true); + if (!parsed) { + console.log("invalid json"); return; + }; if (state.editorSection == "all") { state.resumeObject = parsed; } else { diff --git a/src/cv-maker/static/john-doe.json b/src/cv-maker/static/john-doe.jsonc similarity index 98% rename from src/cv-maker/static/john-doe.json rename to src/cv-maker/static/john-doe.jsonc index 51e1311..f491a31 100644 --- a/src/cv-maker/static/john-doe.json +++ b/src/cv-maker/static/john-doe.jsonc @@ -222,7 +222,8 @@ "id": "docker", "name": "Docker", "category": "devops tool", - "url": "https://en.wikipedia.org/wiki/Docker_(software)" + "url": "https://en.wikipedia.org/wiki/Docker_(software)", + "display": "force" }, { "id": "react", diff --git a/src/cv-maker/static/resume-schema.json b/src/cv-maker/static/resume-schema.json index 8a0bea8..ee1bc56 100644 --- a/src/cv-maker/static/resume-schema.json +++ b/src/cv-maker/static/resume-schema.json @@ -147,6 +147,13 @@ "other" ] }, + "display": { + "type": "string", + "enum": [ + "force", + "hide" + ] + }, "version": { "$ref": "#/$defs/NonEmptyString" },