diff --git a/apps/react-app/public/assets/arrow_select.svg b/apps/react-app/public/assets/arrow_select.svg new file mode 100644 index 00000000..e95f9707 --- /dev/null +++ b/apps/react-app/public/assets/arrow_select.svg @@ -0,0 +1,6 @@ + + + diff --git a/apps/react-app/src/common-app/components/export-config/export-config.component.tsx b/apps/react-app/src/common-app/components/export-config/export-config.component.tsx deleted file mode 100644 index 6767b264..00000000 --- a/apps/react-app/src/common-app/components/export-config/export-config.component.tsx +++ /dev/null @@ -1,126 +0,0 @@ -import React from 'react'; -import { ExportHTMLSettings } from '@lemoncode/manfred2html'; -import { theme } from '@/core/theme'; -import { Button } from '@/common-app/components'; -import * as classes from './export-config.styles'; -import { useUserChoiceContext } from '@/core/user-choice'; -interface Props { - htmlTemplate: string; - cancelExport: () => void; - onExportToHTML: (exportHTMLSettings: ExportHTMLSettings) => void; - onHTMLSettingSelectionChanged: (text: string, exportHTMLSettings: ExportHTMLSettings) => string; -} - -const DOWNLOAD_MESSAGE_TIMEOUT = 2500; - -export const ExportConfig: React.FC = props => { - const { onExportToHTML, cancelExport, htmlTemplate, onHTMLSettingSelectionChanged } = props; - const { userChoice } = useUserChoiceContext(); - const [exportHTMLSettings, setExportHTMLSettings] = React.useState({ - primaryColor: theme.palette.primary[600], - }); - const [isDownloadInProgress, setIsDownloadInProgress] = React.useState(false); - - const [htmlPreview, setHtmlPreview] = React.useState( - onHTMLSettingSelectionChanged(htmlTemplate, exportHTMLSettings) - ); - - const handleColorChange = (event: React.ChangeEvent) => { - setExportHTMLSettings({ primaryColor: event.target.value }); - onHTMLSettingSelectionChanged(htmlTemplate, { primaryColor: event.target.value }); - }; - const handleExportConfigSelection = () => { - setIsDownloadInProgress(true); - onExportToHTML(exportHTMLSettings); - - setTimeout(() => { - cancelExport(); - }, DOWNLOAD_MESSAGE_TIMEOUT); - }; - - React.useEffect(() => { - setHtmlPreview(onHTMLSettingSelectionChanged(htmlTemplate, exportHTMLSettings)); - }, [exportHTMLSettings]); - - return ( -
-
-

SELECCIONA TU TEMA FAVORITO

-
- - - - - - -
-

Ejemplo de previsualización

- -
- - -
- {isDownloadInProgress && ( -
Descarga completada. Revisa tu carpeta de descargas
- )} -
-
- ); -}; diff --git a/apps/react-app/src/common-app/components/index.ts b/apps/react-app/src/common-app/components/index.ts index 1e7a68d9..9db140cf 100644 --- a/apps/react-app/src/common-app/components/index.ts +++ b/apps/react-app/src/common-app/components/index.ts @@ -4,4 +4,4 @@ export * from './button'; export * from './navbar'; export * from './card'; export * from './modal'; -export * from './export-config'; +export * from './select'; diff --git a/apps/react-app/src/common-app/components/modal/modal.styles.ts b/apps/react-app/src/common-app/components/modal/modal.styles.ts index 1070dbcc..9a05a012 100644 --- a/apps/react-app/src/common-app/components/modal/modal.styles.ts +++ b/apps/react-app/src/common-app/components/modal/modal.styles.ts @@ -8,13 +8,17 @@ export const container = css` flex-direction: column; justify-content: center; align-items: center; - gap: 40px; + gap: ${theme.spacing(10)}; position: absolute; z-index: 9999; top: 0; background: rgba(13, 20, 24, 0.75); - overflow: hidden; - + & > :first-child { + max-width: 1400px; + align-self: center; + overflow: hidden; + overflow-y: scroll; + } @media (min-width: 725px) { padding: ${theme.spacing(4)}; } diff --git a/apps/react-app/src/common-app/components/select/customSelect.component.tsx b/apps/react-app/src/common-app/components/select/customSelect.component.tsx new file mode 100644 index 00000000..a0cca9d9 --- /dev/null +++ b/apps/react-app/src/common-app/components/select/customSelect.component.tsx @@ -0,0 +1,38 @@ +import React from 'react'; +import * as classes from './customSelect.styles'; + +interface Props { + listOptions: string[]; + label: string; +} +export const CustomSelect: React.FC = ({ listOptions, label }) => { + const [isOpen, setIsOpen] = React.useState(false); + const [selectedOption, setSelectedOption] = React.useState(label); + + const toggleIsOpen = () => { + setIsOpen(!isOpen); + }; + + const handleOptionSelect = (option: string) => { + setSelectedOption(option); + setIsOpen(false); + }; + return ( +
+
+ {selectedOption || label} + arrow select +
+ {isOpen && ( +
    + {listOptions && + listOptions.map((option, index) => ( +
  • handleOptionSelect(option)}> + {option} +
  • + ))} +
+ )} +
+ ); +}; diff --git a/apps/react-app/src/common-app/components/select/customSelect.styles.ts b/apps/react-app/src/common-app/components/select/customSelect.styles.ts new file mode 100644 index 00000000..df9479cc --- /dev/null +++ b/apps/react-app/src/common-app/components/select/customSelect.styles.ts @@ -0,0 +1,68 @@ +import { css } from '@emotion/css'; +import { theme } from '@/core/theme'; + +export const selectContainer = css` + position: relative; + cursor: pointer; + @media (min-width: 725px) { + flex-grow: 1; + width: 100%; + } +`; +export const selectContent = css` + display: flex; + padding: ${theme.spacing(3)} ${theme.spacing(4)}; + align-items: flex-start; + gap: ${theme.spacing(2)}; + align-self: stretch; + border: 1px solid ${theme.palette.info[600]}; + border-radius: 8px; + background: ${theme.palette.info[50]}; + span { + flex-grow: 1; + color: ${theme.palette.dark[900]}; + font-family: Inter; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 22px; + letter-spacing: 0.15px; + } +`; + +export const listContainer = css` + position: absolute; + width: 100%; + display: flex; + flex-direction: column; + z-index: 999; + padding: ${theme.spacing(2)} ${theme.spacing(0)}; + gap: ${theme.spacing(2)}; + border-radius: 4px; + background: ${theme.palette.info[50]}; + box-shadow: 0px 2px 6px 2px rgba(0, 0, 0, 0.15), 0px 1px 2px 0px rgba(0, 0, 0, 0.3); + li { + display: flex; + height: ${theme.spacing(8)}; + padding: ${theme.spacing(0)} ${theme.spacing(4)}; + justify-content: flex-start; + align-items: center; + + color: ${theme.palette.dark[900]}; + font-family: Inter; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + letter-spacing: 0.25px; + + :hover { + background: ${theme.palette.primary[50]}; + } + } +`; + +export const rotate = (rotate: boolean) => css` + rotate: ${rotate ? '180deg' : '0deg'}; + transition: rotate 0.3s ease; +`; diff --git a/apps/react-app/src/common-app/components/select/index.ts b/apps/react-app/src/common-app/components/select/index.ts new file mode 100644 index 00000000..c77d0ae0 --- /dev/null +++ b/apps/react-app/src/common-app/components/select/index.ts @@ -0,0 +1 @@ +export * from './customSelect.component'; diff --git a/apps/react-app/src/pods/template-export/components/customSelectColor/customSelectColor.component.tsx b/apps/react-app/src/pods/template-export/components/customSelectColor/customSelectColor.component.tsx new file mode 100644 index 00000000..e0527022 --- /dev/null +++ b/apps/react-app/src/pods/template-export/components/customSelectColor/customSelectColor.component.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { theme } from '@/core/theme'; +import * as classes from './customSelectColor.styles'; +import { cx } from '@emotion/css'; + +interface Props { + label: string; + onChange: (event: React.ChangeEvent) => void; +} +type CustomSelectState = 'noInit' | 'open' | 'close'; + +export const CustomSelectColor: React.FC = ({ label, onChange }) => { + const [customSelectState, setCustomSelectState] = React.useState('noInit'); + + const toggleIsOpen = () => { + setCustomSelectState(customSelectState === 'open' ? 'close' : 'open'); + }; + return ( +
+
+ {label} + arrow select +
+
+ + + + + + +
+
+ ); +}; diff --git a/apps/react-app/src/pods/template-export/components/customSelectColor/customSelectColor.styles.ts b/apps/react-app/src/pods/template-export/components/customSelectColor/customSelectColor.styles.ts new file mode 100644 index 00000000..a5abd57c --- /dev/null +++ b/apps/react-app/src/pods/template-export/components/customSelectColor/customSelectColor.styles.ts @@ -0,0 +1,78 @@ +import { css, keyframes } from '@emotion/css'; +import { theme } from '@/core/theme'; + +const openAnimation = keyframes` + 0% { + max-height: 48px; + } + + 100% { + max-height: 500px; + } +`; +const closeAnimation = keyframes` + 0% { + max-height: 500px; + } + + 100% { + max-height: 48px; + } +`; + +export const rootOpenAnimation = css` + animation: ${openAnimation} 0.8s ease forwards; +`; +export const rootCloseAnimation = css` + animation: ${closeAnimation} 0.8s ease forwards; +`; + +export const root = css` + border-bottom: 1px solid ${theme.palette.info[600]}; + overflow: hidden; + max-height: 48px; +`; +export const labelContainer = css` + display: flex; + justify-content: space-between; + padding: ${theme.spacing(3)} ${theme.spacing(4)}; + gap: ${theme.spacing(2)}; + border-radius: 8px; + background: ${theme.palette.info[50]}; + cursor: pointer; + span { + color: ${theme.palette.info[900]}; + font-family: Sanchez; + font-size: 18px; + font-style: normal; + font-weight: 400; + line-height: 24px; + } +`; +export const colorFieldset = css` + background: ${theme.palette.info[50]}; + display: flex; + padding: ${theme.spacing(8)} ${theme.spacing(0)}; + flex-wrap: wrap; + justify-content: center; + gap: ${theme.spacing(8)}; +`; +export const inputRadioButton = (color: string) => css` + appearance: none; + width: 82px; + height: 82px; + box-shadow: 8px 8px 16px 0px rgba(0, 0, 0, 0.25); + border-radius: 50%; + background-color: ${color}; + cursor: pointer; + outline: none; + border: 15px solid ${theme.palette.info[50]}; + &[type='radio']:checked { + box-shadow: 0px 0px 0px 4px ${theme.palette.dark[200]}; + } +`; + +export const rotate = (customSelectState: boolean) => css` + rotate: ${customSelectState ? '180deg' : '0deg'}; + transition: rotate 0.3s ease; +`; diff --git a/apps/react-app/src/pods/template-export/components/customSelectColor/index.ts b/apps/react-app/src/pods/template-export/components/customSelectColor/index.ts new file mode 100644 index 00000000..5e468c64 --- /dev/null +++ b/apps/react-app/src/pods/template-export/components/customSelectColor/index.ts @@ -0,0 +1 @@ +export * from './customSelectColor.component'; diff --git a/apps/react-app/src/pods/template-export/components/export-config/export-config.component.tsx b/apps/react-app/src/pods/template-export/components/export-config/export-config.component.tsx new file mode 100644 index 00000000..007a2a27 --- /dev/null +++ b/apps/react-app/src/pods/template-export/components/export-config/export-config.component.tsx @@ -0,0 +1,84 @@ +import React from 'react'; +import { ExportHTMLSettings } from '@lemoncode/manfred2html'; +import { theme } from '@/core/theme'; +import { Button, CustomSelect } from '@/common-app/components'; +import { useUserChoiceContext } from '@/core/user-choice'; +import { CustomSelectColor } from '../customSelectColor/customSelectColor.component'; +import * as classes from './export-config.styles'; +interface Props { + htmlTemplate: string; + cancelExport: () => void; + onExportToHTML: (exportHTMLSettings: ExportHTMLSettings) => void; + onHTMLSettingSelectionChanged: (text: string, exportHTMLSettings: ExportHTMLSettings) => string; +} + +const DOWNLOAD_MESSAGE_TIMEOUT = 2500; +const OPTIONSDESING = ['Item 1', 'Item 2', 'Item 3']; +const OPTIONSlANGUAGE = ['Item 1', 'Item 2', 'Item 3']; + +export const ExportConfig: React.FC = props => { + const { onExportToHTML, cancelExport, htmlTemplate, onHTMLSettingSelectionChanged } = props; + const { userChoice } = useUserChoiceContext(); + const [exportHTMLSettings, setExportHTMLSettings] = React.useState({ + primaryColor: theme.palette.primary[600], + }); + const [isDownloadInProgress, setIsDownloadInProgress] = React.useState(false); + + const [htmlPreview, setHtmlPreview] = React.useState( + onHTMLSettingSelectionChanged(htmlTemplate, exportHTMLSettings) + ); + + const handleColorChange = (event: React.ChangeEvent) => { + setExportHTMLSettings({ primaryColor: event.target.value }); + onHTMLSettingSelectionChanged(htmlTemplate, { primaryColor: event.target.value }); + }; + const handleExportConfigSelection = () => { + setIsDownloadInProgress(true); + onExportToHTML(exportHTMLSettings); + + setTimeout(() => { + cancelExport(); + }, DOWNLOAD_MESSAGE_TIMEOUT); + }; + + React.useEffect(() => { + setHtmlPreview(onHTMLSettingSelectionChanged(htmlTemplate, exportHTMLSettings)); + }, [exportHTMLSettings]); + + return ( +
+

PERSONALIZA A TU GUSTO

+
+
+
+ + +
+
+ +
+
+
+

Previsualización

+ +
+
+
+ + +
+ {isDownloadInProgress && ( +
Descarga completada. Revisa tu carpeta de descargas
+ )} +
+ ); +}; diff --git a/apps/react-app/src/common-app/components/export-config/export-config.styles.ts b/apps/react-app/src/pods/template-export/components/export-config/export-config.styles.ts similarity index 51% rename from apps/react-app/src/common-app/components/export-config/export-config.styles.ts rename to apps/react-app/src/pods/template-export/components/export-config/export-config.styles.ts index 006128ab..ca59e59a 100644 --- a/apps/react-app/src/common-app/components/export-config/export-config.styles.ts +++ b/apps/react-app/src/pods/template-export/components/export-config/export-config.styles.ts @@ -3,54 +3,28 @@ import { theme } from '@/core/theme'; export const content = css` display: flex; - padding: ${theme.spacing(8)}; + padding: ${theme.spacing(4)}; flex-direction: column; - align-items: center; - gap: ${theme.spacing(16)}; + align-items: stretch; + gap: ${theme.spacing(10)}; flex-grow: 1; width: 100%; background: ${theme.palette.info[50]}; ${theme.typography.mobile.h5}; @media (min-width: 725px) { - border-radius: 16px; ${theme.typography.tablet.h5}; + padding: ${theme.spacing(8)}; + gap: ${theme.spacing(12)}; + flex: 1 0 0; + border-radius: 16px; + align-items: stretch; } @media (min-width: 1024px) { ${theme.typography.desktop.h5}; - } -`; - -export const optionsContainer = css` - display: flex; - flex-direction: column; - flex-grow: 1; - gap: ${theme.spacing(8)}; - width: 100%; -`; - -export const colorFieldset = css` - display: flex; - flex-wrap: wrap; - justify-content: center; - gap: ${theme.spacing(8)}; - width: 100%; - - @media (min-width: 1024px) { - justify-content: start; - } -`; -export const inputRadioButton = (color: string) => css` - appearance: none; - width: 82px; - height: 82px; - box-shadow: 8px 8px 16px 0px rgba(0, 0, 0, 0.25); - border-radius: 50%; - background-color: ${color}; - cursor: pointer; - outline: none; - border: 15px solid ${theme.palette.info[50]}; - &[type='radio']:checked { - box-shadow: 0px 0px 0px 4px ${theme.palette.dark[200]}; + align-items: flex-center; + gap: ${theme.spacing(8)}; + align-self: stretch; + padding: ${theme.spacing(8)}; } `; @@ -70,14 +44,15 @@ export const iframeCV = css` export const buttonContainer = css` display: flex; - justify-content: center; + flex-direction: column; + justify-content: flex-end; align-items: center; gap: ${theme.spacing(8)}; - padding: ${theme.spacing(4)}; - flex-direction: column; + width: 100%; @media (min-width: 1024px) { flex-direction: row; + justify-content: flex-end; } `; @@ -108,3 +83,74 @@ export const downloadMessage = css` ${theme.typography.desktop.h5}; } `; + +export const optionsContainer = css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(10)}; + flex-grow: 1; + @media (min-width: 725px) { + gap: ${theme.spacing(12)}; + } + @media (min-width: 1024px) { + flex-direction: row; + align-items: flex-start; + align-self: stretch; + height: 512px; + } +`; + +export const optionsContent = css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(10)}; + + @media (min-width: 725px) { + gap: ${theme.spacing(12)}; + } + + @media (min-width: 1024px) { + gap: ${theme.spacing(4)}; + } +`; +export const selectContainer = css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(10)}; + + @media (min-width: 725px) { + gap: ${theme.spacing(4)}; + flex-direction: row; + width: 100%; + } + @media (min-width: 1024px) { + width: 274px; + flex-direction: column; + gap: ${theme.spacing(4)}; + } +`; +export const selectColorContainer = css` + @media (min-width: 1024px) { + width: 274px; + flex-direction: column; + gap: ${theme.spacing(4)}; + } +`; +export const prevContainer = css` + display: flex; + flex-direction: column; + gap: ${theme.spacing(10)}; + flex-grow: 1; + + @media (min-width: 725px) { + gap: ${theme.spacing(12)}; + } + + @media (min-width: 1024px) { + flex-direction: column; + align-items: flex-start; + flex: 1 0 0; + align-self: stretch; + gap: ${theme.spacing(8)}; + } +`; diff --git a/apps/react-app/src/common-app/components/export-config/index.ts b/apps/react-app/src/pods/template-export/components/export-config/index.ts similarity index 100% rename from apps/react-app/src/common-app/components/export-config/index.ts rename to apps/react-app/src/pods/template-export/components/export-config/index.ts diff --git a/apps/react-app/src/pods/template-export/template-export.component.tsx b/apps/react-app/src/pods/template-export/template-export.component.tsx index c971269c..5a1a00bb 100644 --- a/apps/react-app/src/pods/template-export/template-export.component.tsx +++ b/apps/react-app/src/pods/template-export/template-export.component.tsx @@ -1,8 +1,9 @@ import React from 'react'; import { ExportHTMLSettings } from '@lemoncode/manfred2html'; import { useUserChoiceContext } from '@/core'; -import { Button, Footer, Header, Modal, Navbar, ExportConfig } from '@/common-app/components'; +import { Button, Footer, Header, Modal, Navbar } from '@/common-app/components'; import * as classes from './template-export.styles'; +import { ExportConfig } from './components/export-config'; interface Props { error: boolean; diff --git a/packages/manfred-common/config/test/jest.js b/packages/manfred-common/config/test/jest.js index 4ce181ab..10d85779 100644 --- a/packages/manfred-common/config/test/jest.js +++ b/packages/manfred-common/config/test/jest.js @@ -7,5 +7,6 @@ export default { moduleNameMapper: { '\\.(jpg|jpeg|png|svg)$': '/config/mocks/fileMock.js', + '^@/(.*)$': '/src/$1', }, }; diff --git a/packages/manfred-common/src/doc-parts/experience-section/experience-section.helpers.spec.ts b/packages/manfred-common/src/doc-parts/experience-section/experience-section.helpers.spec.ts new file mode 100644 index 00000000..c19320ee --- /dev/null +++ b/packages/manfred-common/src/doc-parts/experience-section/experience-section.helpers.spec.ts @@ -0,0 +1,467 @@ +import { mapSortedRolesIntoExperience, sortExperienceByDate, sortRolesByDate } from './experience-section.helpers'; +import { ExperienceVm } from './experience-section.vm'; + +describe('experience-section.helpers specs', () => { + describe('sortRolesByDate', () => { + it('should return roles sorted by starting date so more recent starting date appears first', () => { + // Arrange + const experience: ExperienceVm = { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }; + + // Act + const result = sortRolesByDate(experience); + // Assert + const expectResult: ExperienceVm = { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role2', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role1', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }; + + expect(result).toEqual(expectResult); + }); + }); + + describe('mapSortedRolesIntoExperience', () => { + it('should return experience with sorted roles by starting date so more recent starting date appears first', () => { + // Arrange + const experience: ExperienceVm[] = [ + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role2', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role1', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + { + name: 'name2', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role3', + startDate: '2006-07-09', + finishDate: '2007-10-04', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role4', + startDate: '2011-03-03', + finishDate: '2013-04-02', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + + // Act + const result = mapSortedRolesIntoExperience(experience); + // Assert + const expectResult: ExperienceVm[] = [ + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role2', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role1', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + { + name: 'name2', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role4', + startDate: '2011-03-03', + finishDate: '2013-04-02', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role3', + startDate: '2006-07-09', + finishDate: '2007-10-04', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + + expect(result).toEqual(expectResult); + }); + }); + + describe('sortExperienceByDate', () => { + it('should return experience section sorted by starting date of last role', () => { + // Arrange + const experience: ExperienceVm[] = [ + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + { + name: 'name2', + description: 'description2', + type: 'Autónomo', + roles: [ + { + name: 'role3', + startDate: '2021-01-01', + finishDate: '2023-02-14', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + + // Act + const result = sortExperienceByDate(experience); + // Assert + const expectResult: ExperienceVm[] = [ + { + name: 'name2', + description: 'description2', + type: 'Autónomo', + roles: [ + { + name: 'role3', + startDate: '2021-01-01', + finishDate: '2023-02-14', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + expect(result).toEqual(expectResult); + }); + + it('should return experience section sorted by starting date of last role. No changes if already sorted', () => { + // Arrange + const experience: ExperienceVm[] = [ + { + name: 'name2', + description: 'description2', + type: 'Autónomo', + roles: [ + { + name: 'role3', + startDate: '2021-01-01', + finishDate: '2023-02-14', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + + // Act + const result = sortExperienceByDate(experience); + // Assert + const expectResult: ExperienceVm[] = [ + { + name: 'name2', + description: 'description2', + type: 'Autónomo', + roles: [ + { + name: 'role3', + startDate: '2021-01-01', + finishDate: '2023-02-14', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + expect(result).toEqual(expectResult); + }); + + it('should return same array with one element when array has one element', () => { + // Arrange + const experience: ExperienceVm[] = [ + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + + // Act + const result = sortExperienceByDate(experience); + + // Assert + const expectResult: ExperienceVm[] = [ + { + name: 'name', + description: 'description', + type: 'Autónomo', + roles: [ + { + name: 'role1', + startDate: '2019-02-14', + finishDate: '2020-12-14', + challenges: [ + { + description: 'description', + }, + ], + }, + { + name: 'role2', + startDate: '2015-03-01', + finishDate: '2019-02-13', + challenges: [ + { + description: 'description', + }, + ], + }, + ], + }, + ]; + + expect(result).toEqual(expectResult); + }); + }); +}); diff --git a/packages/manfred-common/src/doc-parts/experience-section/experience-section.helpers.ts b/packages/manfred-common/src/doc-parts/experience-section/experience-section.helpers.ts new file mode 100644 index 00000000..b47a7808 --- /dev/null +++ b/packages/manfred-common/src/doc-parts/experience-section/experience-section.helpers.ts @@ -0,0 +1,16 @@ +import { dateExtractor, sortByDate } from '@/helpers'; +import { ExperienceVm } from './experience-section.vm'; + +export const sortRolesByDate = (experience: ExperienceVm): ExperienceVm => ({ + ...experience, + roles: sortByDate(experience.roles, 'startDate'), +}); + +export const mapSortedRolesIntoExperience = (experience: ExperienceVm[]): ExperienceVm[] => + experience.map(experienceItem => sortRolesByDate(experienceItem)); + +export const sortExperienceByDate = (experience: ExperienceVm[]): ExperienceVm[] => + experience.sort( + (a: ExperienceVm, b: ExperienceVm) => + dateExtractor(b.roles[0], 'startDate') - dateExtractor(a.roles[0], 'startDate') + ); diff --git a/packages/manfred-common/src/doc-parts/experience-section/experience-section.mapper.ts b/packages/manfred-common/src/doc-parts/experience-section/experience-section.mapper.ts index 1ce0b4f7..e1ee5217 100644 --- a/packages/manfred-common/src/doc-parts/experience-section/experience-section.mapper.ts +++ b/packages/manfred-common/src/doc-parts/experience-section/experience-section.mapper.ts @@ -1,6 +1,7 @@ -import { ManfredAwesomicCV } from '../../model'; +import { ManfredAwesomicCV } from '@/model'; import { ExperienceVm, Type } from './experience-section.vm'; import { types } from './experience-section.contants'; +import { mapSortedRolesIntoExperience, sortExperienceByDate } from './experience-section.helpers'; export const mapFromMacCvToExperienceSectionVm = (cv: ManfredAwesomicCV): ExperienceVm[] => { let jobs: ExperienceVm[] = []; @@ -16,7 +17,10 @@ export const mapFromMacCvToExperienceSectionVm = (cv: ManfredAwesomicCV): Experi jobs = [...jobs, { name: organizationName, description: organizationDescription, type: mapType, roles }]; }); - return jobs; + const jobsWithMappedRoles: ExperienceVm[] = mapSortedRolesIntoExperience(jobs); + const jobsSortedByDate: ExperienceVm[] = sortExperienceByDate(jobsWithMappedRoles); + + return jobsSortedByDate; }; export const mapOrganizationType = (type: string, types: Type[]): string => { diff --git a/packages/manfred-common/src/doc-parts/studies-section/studies-section.helpers.spec.ts b/packages/manfred-common/src/doc-parts/studies-section/studies-section.helpers.spec.ts new file mode 100644 index 00000000..5e246cab --- /dev/null +++ b/packages/manfred-common/src/doc-parts/studies-section/studies-section.helpers.spec.ts @@ -0,0 +1,243 @@ +import { sortStudiesByDate } from './studies-section.helpers'; +import { StudiesSectionVm } from './studies-section.vm'; + +describe('studies-section.helpers specs', () => { + describe('sortStudiesByDate', () => { + it('should return studies section sorted by starting date', () => { + // Arrange + const studies: StudiesSectionVm[] = [ + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2020-12-03', + finishDate: '2022-07-08', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2009-04-02', + finishDate: '2011-03-20', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + { + studyType: 'Grado oficial', + degreeAchieved: false, + name: 'name', + startDate: '2022-11-17', + finishDate: '2023-05-01', + description: 'description', + institution: { + name: 'name', + location: { + country: 'España', + region: 'region', + address: 'address', + }, + }, + }, + ]; + + // Act + const result: StudiesSectionVm[] = sortStudiesByDate(studies); + + const expectedResult: StudiesSectionVm[] = [ + { + studyType: 'Grado oficial', + degreeAchieved: false, + name: 'name', + startDate: '2022-11-17', + finishDate: '2023-05-01', + description: 'description', + institution: { + name: 'name', + location: { + country: 'España', + region: 'region', + address: 'address', + }, + }, + }, + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2020-12-03', + finishDate: '2022-07-08', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2009-04-02', + finishDate: '2011-03-20', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + ]; + + // Assert + expect(result).toEqual(expectedResult); + }); + + it('should return studies section sorted by starting date. No changes if already sorted', () => { + // Arrange + const studies: StudiesSectionVm[] = [ + { + studyType: 'Grado oficial', + degreeAchieved: false, + name: 'name', + startDate: '2022-11-17', + finishDate: '2023-05-01', + description: 'description', + institution: { + name: 'name', + location: { + country: 'España', + region: 'region', + address: 'address', + }, + }, + }, + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2020-12-03', + finishDate: '2022-07-08', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + ]; + + // Act + const result: StudiesSectionVm[] = sortStudiesByDate(studies); + + const expectedResult: StudiesSectionVm[] = [ + { + studyType: 'Grado oficial', + degreeAchieved: false, + name: 'name', + startDate: '2022-11-17', + finishDate: '2023-05-01', + description: 'description', + institution: { + name: 'name', + location: { + country: 'España', + region: 'region', + address: 'address', + }, + }, + }, + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2020-12-03', + finishDate: '2022-07-08', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + ]; + + // Assert + expect(result).toEqual(expectedResult); + }); + + it('should return same array with one element when array has one element', () => { + // Arrange + const studies: StudiesSectionVm[] = [ + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2020-12-03', + finishDate: '2022-07-08', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + ]; + + // Act + const result: StudiesSectionVm[] = sortStudiesByDate(studies); + + const expectedResult: StudiesSectionVm[] = [ + { + studyType: 'Certificación', + degreeAchieved: true, + name: 'name2', + startDate: '2020-12-03', + finishDate: '2022-07-08', + description: 'description2', + institution: { + name: 'name2', + location: { + country: 'Italia', + region: 'region2', + address: 'address2', + }, + }, + }, + ]; + + // Assert + expect(result).toEqual(expectedResult); + }); + }); +}); diff --git a/packages/manfred-common/src/doc-parts/studies-section/studies-section.helpers.ts b/packages/manfred-common/src/doc-parts/studies-section/studies-section.helpers.ts new file mode 100644 index 00000000..e13ce8e1 --- /dev/null +++ b/packages/manfred-common/src/doc-parts/studies-section/studies-section.helpers.ts @@ -0,0 +1,5 @@ +import { sortByDate } from '@/helpers'; + +import { StudiesSectionVm } from './studies-section.vm'; + +export const sortStudiesByDate = (studies: StudiesSectionVm[]): StudiesSectionVm[] => sortByDate(studies, 'startDate'); diff --git a/packages/manfred-common/src/doc-parts/studies-section/studies-section.mapper.ts b/packages/manfred-common/src/doc-parts/studies-section/studies-section.mapper.ts index 656d39ab..1bc8ef12 100644 --- a/packages/manfred-common/src/doc-parts/studies-section/studies-section.mapper.ts +++ b/packages/manfred-common/src/doc-parts/studies-section/studies-section.mapper.ts @@ -1,6 +1,7 @@ import { ManfredAwesomicCV } from '@/model'; import { CountryType, Institution, StudiesSectionVm, StudyTypeWithTranslation } from './studies-section.vm'; import { studiesTypes, countryList } from './studies-section.constants'; +import { sortStudiesByDate } from './studies-section.helpers'; export const mapFromMacCvToStudiesSectionVm = (cv: ManfredAwesomicCV): StudiesSectionVm[] => { const studiesMap: StudiesSectionVm[] = @@ -35,7 +36,9 @@ export const mapFromMacCvToStudiesSectionVm = (cv: ManfredAwesomicCV): StudiesSe }; }) ?? []; - return studiesMap; + const studiesSortedByDate: StudiesSectionVm[] = sortStudiesByDate(studiesMap); + + return studiesSortedByDate; }; export const mapStudiesTypes = (studiesType: string, studiesTypes: StudyTypeWithTranslation[]): string => { diff --git a/packages/manfred-common/src/helpers/date-helpers.ts b/packages/manfred-common/src/helpers/date-helpers.ts new file mode 100644 index 00000000..5602f69b --- /dev/null +++ b/packages/manfred-common/src/helpers/date-helpers.ts @@ -0,0 +1,10 @@ +export const dateExtractor = (item: T, path: keyof T): number => { + const date = item[path]; + if (typeof date === 'string') { + return new Date(date).getTime(); + } + throw new Error(`Invalid path: ${path.toString()}`); +}; + +export const sortByDate = (array: T[], path: keyof T): T[] => + array.sort((a: T, b: T) => dateExtractor(b, path) - dateExtractor(a, path)); diff --git a/packages/manfred-common/src/helpers/index.ts b/packages/manfred-common/src/helpers/index.ts new file mode 100644 index 00000000..7a542764 --- /dev/null +++ b/packages/manfred-common/src/helpers/index.ts @@ -0,0 +1 @@ +export * from './date-helpers'; diff --git a/packages/manfred2html/config/test/jest.js b/packages/manfred2html/config/test/jest.js index 4ce181ab..10d85779 100644 --- a/packages/manfred2html/config/test/jest.js +++ b/packages/manfred2html/config/test/jest.js @@ -7,5 +7,6 @@ export default { moduleNameMapper: { '\\.(jpg|jpeg|png|svg)$': '/config/mocks/fileMock.js', + '^@/(.*)$': '/src/$1', }, }; diff --git a/packages/manfred2html/src/helpers/date-helpers.ts b/packages/manfred2html/src/helpers/date-helpers.ts new file mode 100644 index 00000000..5602f69b --- /dev/null +++ b/packages/manfred2html/src/helpers/date-helpers.ts @@ -0,0 +1,10 @@ +export const dateExtractor = (item: T, path: keyof T): number => { + const date = item[path]; + if (typeof date === 'string') { + return new Date(date).getTime(); + } + throw new Error(`Invalid path: ${path.toString()}`); +}; + +export const sortByDate = (array: T[], path: keyof T): T[] => + array.sort((a: T, b: T) => dateExtractor(b, path) - dateExtractor(a, path)); diff --git a/packages/manfred2html/src/helpers/index.ts b/packages/manfred2html/src/helpers/index.ts new file mode 100644 index 00000000..7a542764 --- /dev/null +++ b/packages/manfred2html/src/helpers/index.ts @@ -0,0 +1 @@ +export * from './date-helpers'; diff --git a/packages/manfred2md/config/test/jest.js b/packages/manfred2md/config/test/jest.js index 4ce181ab..10d85779 100644 --- a/packages/manfred2md/config/test/jest.js +++ b/packages/manfred2md/config/test/jest.js @@ -7,5 +7,6 @@ export default { moduleNameMapper: { '\\.(jpg|jpeg|png|svg)$': '/config/mocks/fileMock.js', + '^@/(.*)$': '/src/$1', }, }; diff --git a/packages/manfred2md/src/helpers/date-helpers.ts b/packages/manfred2md/src/helpers/date-helpers.ts new file mode 100644 index 00000000..5602f69b --- /dev/null +++ b/packages/manfred2md/src/helpers/date-helpers.ts @@ -0,0 +1,10 @@ +export const dateExtractor = (item: T, path: keyof T): number => { + const date = item[path]; + if (typeof date === 'string') { + return new Date(date).getTime(); + } + throw new Error(`Invalid path: ${path.toString()}`); +}; + +export const sortByDate = (array: T[], path: keyof T): T[] => + array.sort((a: T, b: T) => dateExtractor(b, path) - dateExtractor(a, path)); diff --git a/packages/manfred2md/src/helpers/index.ts b/packages/manfred2md/src/helpers/index.ts new file mode 100644 index 00000000..7a542764 --- /dev/null +++ b/packages/manfred2md/src/helpers/index.ts @@ -0,0 +1 @@ +export * from './date-helpers'; diff --git a/packages/manfred2word/config/test/jest.js b/packages/manfred2word/config/test/jest.js index 4ce181ab..10d85779 100644 --- a/packages/manfred2word/config/test/jest.js +++ b/packages/manfred2word/config/test/jest.js @@ -7,5 +7,6 @@ export default { moduleNameMapper: { '\\.(jpg|jpeg|png|svg)$': '/config/mocks/fileMock.js', + '^@/(.*)$': '/src/$1', }, }; diff --git a/packages/manfred2word/src/helpers/date-helpers.ts b/packages/manfred2word/src/helpers/date-helpers.ts new file mode 100644 index 00000000..5602f69b --- /dev/null +++ b/packages/manfred2word/src/helpers/date-helpers.ts @@ -0,0 +1,10 @@ +export const dateExtractor = (item: T, path: keyof T): number => { + const date = item[path]; + if (typeof date === 'string') { + return new Date(date).getTime(); + } + throw new Error(`Invalid path: ${path.toString()}`); +}; + +export const sortByDate = (array: T[], path: keyof T): T[] => + array.sort((a: T, b: T) => dateExtractor(b, path) - dateExtractor(a, path)); diff --git a/packages/manfred2word/src/helpers/index.ts b/packages/manfred2word/src/helpers/index.ts new file mode 100644 index 00000000..7a542764 --- /dev/null +++ b/packages/manfred2word/src/helpers/index.ts @@ -0,0 +1 @@ +export * from './date-helpers';