From c6f23a9dc1e0b4a592d94f1061ba4e7e75d9972c Mon Sep 17 00:00:00 2001 From: "mike.toggweiler" Date: Mon, 27 Jan 2025 15:51:33 +0100 Subject: [PATCH] remove tegonal specific content, made app customizable --- .env.example | 11 + .github/workflows/publish.yml | 2 + public/logo.png | Bin 0 -> 1488 bytes public/tegonal.svg | 479 ------------------ .../cv/[id]/(lib)/components/highlight.tsx | 15 +- src/app/cv/[id]/default_page.scss | 103 ++++ src/app/cv/[id]/default_page.tsx | 302 +++++++++++ src/app/cv/[id]/globals.scss | 108 ---- src/app/cv/[id]/page.tsx | 341 ++----------- src/app/cv/[id]/utilities.ts | 40 ++ 10 files changed, 500 insertions(+), 901 deletions(-) create mode 100644 public/logo.png delete mode 100644 public/tegonal.svg create mode 100644 src/app/cv/[id]/default_page.scss create mode 100644 src/app/cv/[id]/default_page.tsx create mode 100644 src/app/cv/[id]/utilities.ts diff --git a/.env.example b/.env.example index 133ccb0..0003bd9 100644 --- a/.env.example +++ b/.env.example @@ -27,3 +27,14 @@ SMTP_HOST=localhost SMTP_PORT=1025 SMTP_USER=smtpuser SMTP_PASS=smtppwd + +# Company name used in default page +DEFAULT_PAGE_COMPANY_NAME=MyCompany +DEFAULT_PAGE_COMPANY_ADDRESS=Homestreet 20 +DEFAULT_PAGE_COMPANY_CITY=3000 Bern +DEFAULT_PAGE_COMPANY_URL=https://github.com/tegonal + +# Provide custom logo in the public folder +DEFAULT_PAGE_COMPANY_LOGO=logo.png +DEFAULT_PAGE_COMPANY_LOGO_WIDTH=20 +DEFAULT_PAGE_COMPANY_LOGO_HEIGHT=20 \ No newline at end of file diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 90c680e..025c379 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -7,6 +7,8 @@ on: jobs: build-and-push-image: + # Run only in non-forked repository + if: github.repository == 'tegonal/cv-manager' runs-on: ubuntu-latest permissions: contents: read diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..9634e7efa8e373d4c82d8b55899cf2370e9c8ce3 GIT binary patch literal 1488 zcmV;>1uy!EP)Px)ib+I4R9HvNS7}UCR}}spi@+cPqJW^%K^Tm3MjZxF$7W=D*Ql0T{@&lwL(--f?A;(L<$m+rKq5Y0}jY*&SiKo&M1Z-(C^oK z_nvd^ch7e3c|$tPgo@6W&L@7HjGgN+TAIyk__+#Cb1U!$R;0~f2R@Jyl5khL%|!D6u( zMpLHXuf|3+J$a%bGu6-#-?_PAo|P3ky1P+u{W_k!cmb|kq2LqGo<-uhb4)Ld<`Sr3I#TwIDvDOm2A#bB*KwRn-H*aCG>T5+1TsBK}6^0BS~eC#%MDaqoXlr z#tgyczt5jzc}xtrin=L|(pUkA{)vr%#bqRD{Zg2Fx-u z!_Ql{G8Yq;L`6YlV1PekV?p7|x_lX*N+kG1EEeqKl$1c4o{m}O=4j2x5XOTuGhwK& zk5s7?eM;p}5>}_A;N!V-1)=*bEo=uABLcbiL24>Ku(V|VPCIsBrl~0sHf%s>Ru*nQ ze2B&N_PCjpB-kjgt;MpK7&zM6qUPvPVf;s#4ELIw8F^L(K^Yl*Mo^H32WV#xJ37$Q z*T-UKWopV~F5kTi&-i!^dGo1L(V3ksj9;y(!DoB-!o<)JT{$_>)zM*de>XKD_S7ky z-L-3oKvz!>Z)R-Fekj|?onN}Rz;5PDwM7O72a$j68bY(P@mxzK%(k#Vb9%ay8~`|HV;J_`=y)n4>&0F)|t=z*EMkAS)@s_bMZEEG!_gvB5MWBap(K#zr9|8ruip z>Ewh&g2>^ymLTm-15pJr#Xa1n0Q)v;228!D5M;A(G=f7;te2y9ME!^taG#zlad z(TxKK1YK%syK-~2+M<@+DU-3%{-dgDgaDNmmDqT&$J-l;{{DjM+QWxgm(zwd9v=90 z=T0_tSS}wSKuv?1$9V8&nb~dIru9G~+rNc};~Q63whR#!a$t2r0xsOSGv@jR`ueOT zKYRI-$x{5gWHOLz36yxo0RdX=INRCb_Mt=2;W#LiN>~L3jt~gX%|)_Y{!RiEw+m5G zD7$|jv8PWn*@bp?s7g*2^jf>RU>h8)VP}e-90Cj+{fJ=NgFq^%LHLnTpUVOO=gi;jN1nfs*%mSbO}Ed6+t;LfTa=% zc6)o{Lt9%xCcmr<8&Xr*zgj1xfyT#=nQ=OVY^P7hG6x4(stzKWm*2XDZ$~XiVQI_E z1l@|>2M^lXSdZxJ=@}w0`Y;;Hz{Tonmbv8S(FpPKLfV!sW2vLEz8*fw$!O{96l6JZ zNC;2vUE}V~GFlrRw6@~l`SZvrEq!Z_{NU-ydi`i`4cn@D^ayFi#h}p8i8~s?t(aW6 z!qE|QXiO7{Sp10T&HDQHKAkO_B{9VivSQhfsa(-dHx^o|5vLf`ilwx0000c*h} literal 0 HcmV?d00001 diff --git a/public/tegonal.svg b/public/tegonal.svg deleted file mode 100644 index 553b5db..0000000 --- a/public/tegonal.svg +++ /dev/null @@ -1,479 +0,0 @@ - - - -image/svg+xml15 Jahre ! \ No newline at end of file diff --git a/src/app/cv/[id]/(lib)/components/highlight.tsx b/src/app/cv/[id]/(lib)/components/highlight.tsx index f9eea2f..c019ce1 100644 --- a/src/app/cv/[id]/(lib)/components/highlight.tsx +++ b/src/app/cv/[id]/(lib)/components/highlight.tsx @@ -5,11 +5,22 @@ type Props = { title: string | undefined | null; subtitle: string | undefined | null; description: any; + borderLeftColor?: string; }; -export const HighlightEntry: React.FC = ({ title, subtitle, description }) => { +export const HighlightEntry: React.FC = ({ + title, + subtitle, + description, + borderLeftColor, +}) => { return ( -
+

{title}

{subtitle}

diff --git a/src/app/cv/[id]/default_page.scss b/src/app/cv/[id]/default_page.scss new file mode 100644 index 0000000..32fa2da --- /dev/null +++ b/src/app/cv/[id]/default_page.scss @@ -0,0 +1,103 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +#companyLogo { + position: running(companyLogo); +} + +#companyLogo img { + display: inline-block; + height: auto; + width: auto; +} + +#pageFooter { + position: running(pageFooter); +} + +#pageNumber::before { + counter-increment: page; + content: ' ' counter(page); +} + +/* typography */ + +body { + width: 100%; + height: 100%; + padding: 0; + margin: 0; + counter-reset: page; + font-size: 8pt; + --primary-color: #005f4b; + --secondary-color: #666; + --spacer-hr-color: #999; + --highlight-background-color: #f8f8f8; + font-family: sans-serif; + font-size: 10pt; + font-weight: 300; + color: black; +} + +a { + &:link { + @apply underline; + color: black; + } + &:hover { + @apply no-underline; + } + &:focus { + } + &:visited { + color: black; + } +} + +p { + line-height: 133%; + + &.margin { + margin: 0 0 12pt 0; + } + + &.small-margin { + margin: 0 0 6pt 0; + } +} + +.lead { + font-size: 12pt; + line-height: 133%; +} + +.small { + font-size: 7.5pt; +} + +.additional { + font-style: italic; +} + +h1 { + font-size: 24pt; + font-weight: 300; + margin-top: 0; +} + +h2 { + font-size: 18pt; + font-weight: 300; + margin-top: 0; +} + +h3 { + font-size: 7pt; + font-weight: 700; + margin-top: 0; +} + +ul { + padding-left: 0; +} diff --git a/src/app/cv/[id]/default_page.tsx b/src/app/cv/[id]/default_page.tsx new file mode 100644 index 0000000..ae755f8 --- /dev/null +++ b/src/app/cv/[id]/default_page.tsx @@ -0,0 +1,302 @@ +import React from 'react'; +import Image from 'next/image'; +import { PayloadLexicalReactRenderer } from '@/lib/lexical-render/src/payloadLexicalReactRenderer'; +import { Company, Level, Project, Skill, SkillGroup } from '@/types/payload-types'; +import { I18nCollection } from '@/lib/i18nCollection'; +import { HighlightEntry } from '@/app/cv/[id]/(lib)/components/highlight'; +import { capitalize, isEmpty } from 'lodash-es'; +import { CvPageProps } from './page'; +import { filterEmptyLexicalNodes, formatDate, fromToYear, hasLexicalNodes } from './utilities'; +import * as process from 'node:process'; +import './default_page.scss'; + +const DefaultPage: React.FC = async ({ + cv, + profileImageDataUrl, + hasOverride, + exportOverride, + locale, +}) => { + return ( + <> +
+ {process.env.DEFAULT_PAGE_COMPANY_NAME +
+
+
+
+

+ {process.env.DEFAULT_PAGE_COMPANY_NAME + ' - ' || ''} + {process.env.DEFAULT_PAGE_COMPANY_ADDRESS + ' - ' || ''} + {process.env.DEFAULT_PAGE_COMPANY_CITY + ' - ' || ''} + {process.env.DEFAULT_PAGE_COMPANY_URL + ' - ' || ''} +

+
+
+

+
+
+
+ {/* Basic Profile */} +
+
+
+
+
+

{cv.fullName}

+

{cv.jobTitle}

+
+
+
+ {cv.fullName} +
+
+
+

{I18nCollection.fieldLabel.introduction[locale]}

+ +
+
+
+
+
+
+

{I18nCollection.fieldLabel.profile[locale]}

+ {cv.birthday && hasOverride('birthday') && ( +
+

{I18nCollection.fieldLabel.birthday[locale]}

+

{formatDate(cv.birthday, locale)}

+
+ )} + {cv.nationalityStatus && hasOverride('nationalityStatus') && ( +
+

{I18nCollection.fieldLabel.nationalityStatus[locale]}

+

{cv.nationalityStatus}

+
+ )} + {cv.phoneNumber && hasOverride('phoneNumber') && ( +
+

{I18nCollection.fieldLabel.phoneNumber[locale]}

+

{cv.phoneNumber}

+
+ )} + {cv.email && hasOverride('email') && ( +
+

{I18nCollection.fieldLabel.email[locale]}

+

{cv.email}

+
+ )} + {!isEmpty(cv.links) && hasOverride('links') && ( +
+

{I18nCollection.fieldLabel.links[locale]}

+ {cv.links?.map((link) => ( + + ))} +
+ )} +
+
+

{I18nCollection.fieldLabel.education[locale]}

+ {!isEmpty(cv.eduHighlights) && ( +
+ {cv.eduHighlights?.map((item) => ( + + ))} +
+ )} + + {!isEmpty(cv.edu) && ( +
+ {cv.edu?.map((item) => ( +
+

{item.institution}

+

{fromToYear(locale, item.fromYear, item.toYear)}

+
+ +
+
+ ))} +
+ )} + + {!isEmpty(cv.certs) && ( +
+

{I18nCollection.fieldLabel.certifications[locale]}

+ {cv.certs?.map((item) => ( +
+

{item.name}

+

{fromToYear(locale, item.toYear)}

+
+ +
+
+ ))} +
+ )} + + {!isEmpty(cv.courses) && ( +
+

{I18nCollection.fieldLabel.courses[locale]}

+ {cv.courses?.map((item) => ( +
+

{item.name}

+

{fromToYear(locale, item.toYear)}

+
+ +
+
+ ))} +
+ )} +
+
+
+ {/* Skills */} +
+
+
+

{I18nCollection.fieldLabel.skills[locale]}

+ {!isEmpty(cv.skillHighlights) && ( +
+ {cv.skillHighlights?.map((item) => ( + + ))} +
+ )} + {cv.skillGroups?.map((group) => { + if (group.skills && group.skills.length < 1) return null; + return ( +
+
+

{(group.group as SkillGroup).name}

+ {group.skillGroupDescription && ( +
+ +
+ )} +
+
+ {group.skills?.map((item) => ( +
+

+ {(item.skill.value as Skill | SkillGroup).name} +

+ {item.level &&

{(item.level as Level).level}

} + {item['sub-skill'] && !isEmpty(item['sub-skill']) && ( +

+ {item['sub-skill'].map((i) => (i as Skill).name).join(', ')} +

+ )} +
+ ))} +
+
+ ); + })} + {!isEmpty(cv.otherSkills) && ( +
+

{I18nCollection.fieldLabel.otherSkills[locale]}

+
+ {cv.otherSkills?.map((item) => ( +
+

{item.name}

+

{(item.level as Level).level}

+
+ ))} +
+
+ )} + {!isEmpty(cv.lang) && ( +
+

{I18nCollection.fieldLabel.languages[locale]}

+
+ {cv.lang?.map((item) => ( +
+

{(item.language as Skill).name}

+

{(item.level as Level).level}

+
+ ))} +
+
+ )} +
+
+
+ {/* Projects and Work Experience */} +
+
+
+

{I18nCollection.fieldLabel.workExperience[locale]}

+ {!isEmpty(cv.jobHighlights) && ( +
+ {cv.jobHighlights?.map((item) => ( + + ))} +
+ )} + {!isEmpty(cv.projects) && ( +
+

{I18nCollection.fieldLabel.projects[locale]}

+ {cv.projects?.map((item) => { + const projectKey = `project_${item.id}`; + if (projectKey in exportOverride && !exportOverride[projectKey]) return null; + return ( +
+

{(item.company as Company).name}

+

{(item.project as Project).name}

+

+ {fromToYear(locale, item.fromYear, item.toYear)} +

+
+ +
+
+ ); + })} +
+ )} +
+ {hasLexicalNodes(cv.casualInfo as any) && hasOverride('casualInfo') && ( +
+

{I18nCollection.fieldLabel.casualInfo[locale]}

+
+ +
+
+ )} +
+
+ + ); +}; + +export default DefaultPage; diff --git a/src/app/cv/[id]/globals.scss b/src/app/cv/[id]/globals.scss index 3f0ef4a..132bb1c 100644 --- a/src/app/cv/[id]/globals.scss +++ b/src/app/cv/[id]/globals.scss @@ -20,38 +20,6 @@ html { height: 100%; } -body { - width: 100%; - height: 100%; - padding: 0; - margin: 0; - counter-reset: page; - font-size: 8pt; - --primary-color: #C0D72E; - --secondary-color: #666; - --spacer-hr-color: #999; - --highlight-background-color: #f8f8f8; -} - -#companyLogo { - position: running(companyLogo); -} - -#companyLogo img { - display: inline-block; - width: 36mm; - height: auto; -} - -#pageFooter { - position: running(pageFooter); -} - -#pageNumber::before { - counter-increment: page; - content: " " counter(page); -} - .break-after { break-after: always; } @@ -59,79 +27,3 @@ body { .no-page-break { page-break-inside: avoid; } - -.circle-mask { - clip-path: circle(50% at 50% 50%); -} - - -/* typography */ - -body { - font-family: Rubik, Arial, sans-serif; - font-size: 9pt; - font-weight: 300; - color: black; -} - -a { - &:link { - @apply underline; - color: black; - } - &:hover { - @apply no-underline; - } - &:focus { - } - &:visited{ - color: black; - } -} - -p { - line-height: 133%; - - &.margin { - margin: 0 0 12pt 0; - } - - &.small-margin { - margin: 0 0 6pt 0; - } -} - -.lead { - font-size: 12pt; - line-height: 133%; -} - -.small { - font-size: 7.5pt; -} - -.additional { - font-style: italic; -} - -h1 { - font-size: 24pt; - font-weight: 300; - margin-top: 0; -} - -h2 { - font-size: 18pt; - font-weight: 300; - margin-top: 0; -} - -h3 { - font-size: 7pt; - font-weight: 700; - margin-top: 0; -} - -ul { - padding-left: 0; -} diff --git a/src/app/cv/[id]/page.tsx b/src/app/cv/[id]/page.tsx index 5386e4f..1feecc4 100644 --- a/src/app/cv/[id]/page.tsx +++ b/src/app/cv/[id]/page.tsx @@ -1,18 +1,13 @@ import React from 'react'; import configPromise from '@payload-config'; -import Image from 'next/image'; -import { PayloadLexicalReactRenderer } from '@/lib/lexical-render/src/payloadLexicalReactRenderer'; -import { Company, Level, Media, Project, Skill, SkillGroup } from '@/types/payload-types'; -import { I18nCollection } from '@/lib/i18nCollection'; +import { Cv, Media } from '@/types/payload-types'; import { decodeFromBase64 } from 'next/dist/build/webpack/loaders/utils'; -import { PayloadLexicalReactRendererContent } from '@/payload/utilities/lexical-render/src/payloadLexicalReactRenderer'; -import { HighlightEntry } from '@/app/cv/[id]/(lib)/components/highlight'; -import { capitalize, isEmpty } from 'lodash-es'; -import { getPayload } from 'payload'; +import { getPayload, TypedLocale } from 'payload'; import ky from 'ky'; import { headers } from 'next/headers'; import * as process from 'node:process'; import { PRINTER_HEADER_KEY } from '@/payload/utilities/constants'; +import DefaultPage from './default_page'; type Args = { params: Promise<{ @@ -24,46 +19,17 @@ type Args = { }; type DecodedSearchParams = { - locale: 'de'; + locale: TypedLocale; exportOverride: Record; secret: string; }; -const formatDate = (date: string, locale: string) => { - const dateObj = new Date(date); - return dateObj.toLocaleDateString(locale); -}; - -const formatYear = (date: string) => { - const dateObj = new Date(date); - return dateObj.getFullYear().toString(); -}; - -const fromToYear = (locale: 'de' | 'en', from?: string | null, to?: string | null) => { - if (!from) return ''; - let returnString = formatYear(from); - if (!to) { - returnString = `${I18nCollection.fieldLabel.since[locale]} ${returnString}`; - } else if (from != to) { - returnString = `${returnString} - ${formatYear(to)}`; - } - return returnString; -}; - -// Recursively check nodes of lexical content that have empty children -const filterEmptyLexicalNodes = (data: PayloadLexicalReactRendererContent) => { - if (data.root.children.length > 0) { - data.root.children = data.root.children.filter((child) => { - return !('children' in child && child.children.length === 0); - }); - } - return data; -}; - -const hasLexicalNodes = (data: PayloadLexicalReactRendererContent) => { - if (!data) return false; - const filtered = filterEmptyLexicalNodes(data); - return filtered.root.children.length > 0; +export type CvPageProps = { + cv: Cv; + profileImageDataUrl: string; + hasOverride: (key: string) => boolean; + exportOverride: Record; + locale: TypedLocale; }; const Page = async ({ params, searchParams }: Args) => { @@ -75,7 +41,6 @@ const Page = async ({ params, searchParams }: Args) => { params: await params, searchParams: await searchParams, }; - const locale = 'de'; const headersList = await headers(); const printerSecret = headersList.get(PRINTER_HEADER_KEY); @@ -95,6 +60,7 @@ const Page = async ({ params, searchParams }: Args) => { const decodedParams: DecodedSearchParams = decodeFromBase64(query.searchParams.p as string); const { exportOverride } = decodedParams; + const locale: TypedLocale = decodedParams.locale; const cv = await payload .find({ @@ -128,275 +94,26 @@ const Page = async ({ params, searchParams }: Args) => { return key in exportOverride && exportOverride[key]; }; - return ( - <> -
- {'Tegonal -
-
-
-
-

Tegonal Genossenschaft - Wasserwerkgasse 2 - 3011 Bern - tegonal.com

-
-
-

-
-
-
- {/* Basic Profile */} -
-
-
-
- {cv.fullName} -
-
-
-

{cv.fullName}

-

{cv.jobTitle}

-
-
-
-
-

{I18nCollection.fieldLabel.introduction[locale]}

- -
-
-
-
-
-
-

{I18nCollection.fieldLabel.profile[locale]}

- {cv.birthday && hasOverride('birthday') && ( -
-

{I18nCollection.fieldLabel.birthday[locale]}

-

{formatDate(cv.birthday, locale)}

-
- )} - {cv.nationalityStatus && hasOverride('nationalityStatus') && ( -
-

{I18nCollection.fieldLabel.nationalityStatus[locale]}

-

{cv.nationalityStatus}

-
- )} - {cv.phoneNumber && hasOverride('phoneNumber') && ( -
-

{I18nCollection.fieldLabel.phoneNumber[locale]}

-

{cv.phoneNumber}

-
- )} - {cv.email && hasOverride('email') && ( -
-

{I18nCollection.fieldLabel.email[locale]}

-

{cv.email}

-
- )} - {!isEmpty(cv.links) && hasOverride('links') && ( -
-

{I18nCollection.fieldLabel.links[locale]}

- {cv.links?.map((link) => ( - - ))} -
- )} -
-
-

{I18nCollection.fieldLabel.education[locale]}

- {!isEmpty(cv.eduHighlights) && ( -
- {cv.eduHighlights?.map((item) => ( - - ))} -
- )} - - {!isEmpty(cv.edu) && ( -
- {cv.edu?.map((item) => ( -
-

{item.institution}

-

{fromToYear(locale, item.fromYear, item.toYear)}

-
- -
-
- ))} -
- )} + const tryRequire = (path: string) => { + try { + return require(`${path}`); + } catch (err) { + return null; + } + }; - {!isEmpty(cv.certs) && ( -
-

{I18nCollection.fieldLabel.certifications[locale]}

- {cv.certs?.map((item) => ( -
-

{item.name}

-

{fromToYear(locale, item.toYear)}

-
- -
-
- ))} -
- )} + const CvPage: React.FC = tryRequire('./custom_page') + ? tryRequire('./custom_page').default + : DefaultPage; - {!isEmpty(cv.courses) && ( -
-

{I18nCollection.fieldLabel.courses[locale]}

- {cv.courses?.map((item) => ( -
-

{item.name}

-

{fromToYear(locale, item.toYear)}

-
- -
-
- ))} -
- )} -
-
-
- {/* Skills */} -
-
-
-

{I18nCollection.fieldLabel.skills[locale]}

- {!isEmpty(cv.skillHighlights) && ( -
- {cv.skillHighlights?.map((item) => ( - - ))} -
- )} - {cv.skillGroups?.map((group) => { - if (group.skills && group.skills.length < 1) return null; - return ( -
-
-

{(group.group as SkillGroup).name}

- {group.skillGroupDescription && ( -
- -
- )} -
-
- {group.skills?.map((item) => ( -
-

- {(item.skill.value as Skill | SkillGroup).name} -

- {item.level &&

{(item.level as Level).level}

} - {item['sub-skill'] && !isEmpty(item['sub-skill']) && ( -

- {item['sub-skill'].map((i) => (i as Skill).name).join(', ')} -

- )} -
- ))} -
-
- ); - })} - {!isEmpty(cv.otherSkills) && ( -
-

{I18nCollection.fieldLabel.otherSkills[locale]}

-
- {cv.otherSkills?.map((item) => ( -
-

{item.name}

-

{(item.level as Level).level}

-
- ))} -
-
- )} - {!isEmpty(cv.lang) && ( -
-

{I18nCollection.fieldLabel.languages[locale]}

-
- {cv.lang?.map((item) => ( -
-

{(item.language as Skill).name}

-

{(item.level as Level).level}

-
- ))} -
-
- )} -
-
-
- {/* Projects and Work Experience */} -
-
-
-

{I18nCollection.fieldLabel.workExperience[locale]}

- {!isEmpty(cv.jobHighlights) && ( -
- {cv.jobHighlights?.map((item) => ( - - ))} -
- )} - {!isEmpty(cv.projects) && ( -
-

{I18nCollection.fieldLabel.projects[locale]}

- {cv.projects?.map((item) => { - const projectKey = `project_${item.id}`; - if (projectKey in exportOverride && !exportOverride[projectKey]) return null; - return ( -
-

{(item.company as Company).name}

-

{(item.project as Project).name}

-

- {fromToYear(locale, item.fromYear, item.toYear)} -

-
- -
-
- ); - })} -
- )} -
- {hasLexicalNodes(cv.casualInfo as any) && hasOverride('casualInfo') && ( -
-

{I18nCollection.fieldLabel.casualInfo[locale]}

-
- -
-
- )} -
-
+ return ( + <> + ); }; diff --git a/src/app/cv/[id]/utilities.ts b/src/app/cv/[id]/utilities.ts new file mode 100644 index 0000000..b426033 --- /dev/null +++ b/src/app/cv/[id]/utilities.ts @@ -0,0 +1,40 @@ +import { I18nCollection } from '@/lib/i18nCollection'; +import { PayloadLexicalReactRendererContent } from '@/payload/utilities/lexical-render/src/payloadLexicalReactRenderer'; +import { TypedLocale } from 'payload'; + +export const formatDate = (date: string, locale: string) => { + const dateObj = new Date(date); + return dateObj.toLocaleDateString(locale); +}; + +export const formatYear = (date: string) => { + const dateObj = new Date(date); + return dateObj.getFullYear().toString(); +}; + +export const fromToYear = (locale: TypedLocale, from?: string | null, to?: string | null) => { + if (!from) return ''; + let returnString = formatYear(from); + if (!to) { + returnString = `${I18nCollection.fieldLabel.since[locale]} ${returnString}`; + } else if (from != to) { + returnString = `${returnString} - ${formatYear(to)}`; + } + return returnString; +}; + +// Recursively check nodes of lexical content that have empty children +export const filterEmptyLexicalNodes = (data: PayloadLexicalReactRendererContent) => { + if (data.root.children.length > 0) { + data.root.children = data.root.children.filter((child) => { + return !('children' in child && child.children.length === 0); + }); + } + return data; +}; + +export const hasLexicalNodes = (data: PayloadLexicalReactRendererContent) => { + if (!data) return false; + const filtered = filterEmptyLexicalNodes(data); + return filtered.root.children.length > 0; +};