├── src ├── env.d.ts ├── utils │ ├── eventBus.ts │ ├── scrollServices.ts │ ├── theme.ts │ ├── resumePaths.ts │ ├── urlService.ts │ ├── localStorage.ts │ └── pdf │ │ ├── resumePDF.ts │ │ ├── introductionPDF.ts │ │ └── sectionPDF.ts ├── store.ts ├── components │ ├── shared │ │ ├── Paragraph │ │ │ └── CustomParagraph.astro │ │ ├── Subsection │ │ │ ├── SubsectionAlign.vue │ │ │ └── SubsectionCard.vue │ │ ├── Anchor │ │ │ ├── BasicEmail.vue │ │ │ └── BasicLink.vue │ │ ├── Tooltip │ │ │ └── TooltipWrapper.vue │ │ ├── Button │ │ │ ├── ModalButton.vue │ │ │ ├── BasicButton.vue │ │ │ ├── IconButton.vue │ │ │ ├── CircleButtonWithIcon.vue │ │ │ ├── CircleButton.vue │ │ │ └── CloseAddButton.vue │ │ ├── Transition │ │ │ ├── AppearFadeTransition.vue │ │ │ ├── ListTransition.vue │ │ │ └── AppearFadePanelTransition.vue │ │ ├── Icon │ │ │ └── IconByUrl.vue │ │ ├── others │ │ │ ├── ModalTemplate.vue │ │ │ └── ShakeTemplate.vue │ │ ├── Error │ │ │ └── ErrorsSection.vue │ │ ├── checkbox │ │ │ └── SwitchCheckbox.vue │ │ ├── TextArea │ │ │ ├── AdaptableTextArea.vue │ │ │ └── BasicTextArea.vue │ │ └── Modal │ │ │ └── ConfirmationModal.vue │ ├── CVExampleImages.astro │ ├── NavigationLink.astro │ ├── Title.astro │ ├── Subtitle.astro │ ├── Navigation.astro │ ├── app │ │ ├── Preview │ │ │ ├── PreviewSectionHeader.vue │ │ │ ├── PreviewSection.vue │ │ │ ├── PreviewResume.vue │ │ │ ├── PreviewIntroduction.vue │ │ │ └── PreviewSubsection.vue │ │ ├── Editor │ │ │ ├── EditorBar.vue │ │ │ ├── List │ │ │ │ ├── EditorListSection.vue │ │ │ │ ├── EditorElements.vue │ │ │ │ └── EditorElement.vue │ │ │ ├── TimeInterval │ │ │ │ ├── EditorSiteSection.vue │ │ │ │ └── EditorTimeInterval.vue │ │ │ ├── Section │ │ │ │ └── EditorSection.vue │ │ │ ├── EditorResume.vue │ │ │ ├── Introduction │ │ │ │ ├── EditorSocialAccount.vue │ │ │ │ └── EditorIntroduction.vue │ │ │ └── Subsection │ │ │ │ └── EditorSubsection.vue │ │ └── Section │ │ │ └── CreateSectionModal.vue │ ├── Header.astro │ ├── Social.astro │ ├── StaticImage.astro │ ├── Contacts.astro │ ├── CVEditor.vue │ ├── ResumeElement.vue │ ├── CardImage.astro │ ├── ResumesList.vue │ └── ThemeIcon.astro ├── icons │ ├── trash.svg │ ├── edit.svg │ ├── linkedin.svg │ ├── twitter.svg │ └── github.svg ├── models │ ├── SubsectionElement.ts │ ├── SubsectionTimeInterval.ts │ ├── Resume.ts │ ├── SocialAccount.ts │ ├── Section.ts │ ├── Introduction.ts │ ├── Subsection.ts │ └── SectionTemplate.ts ├── composables │ ├── useOpenModal.ts │ └── useDrag.ts ├── pages │ ├── editor.astro │ ├── editor │ │ └── [id].astro │ ├── index.astro │ └── about.astro ├── layouts │ ├── TitleLayout.astro │ └── BaseLayout.astro ├── styles │ └── global.css ├── stores │ ├── ResumeStore.ts │ ├── SubsectionStore.ts │ ├── SectionStore.ts │ └── IntroductionStore.ts └── extensions │ └── extensions.ts ├── public ├── cv.webp ├── cv2.webp ├── google0552f88e2da450de.html ├── project.webp ├── fonts │ ├── fontello.ttf │ ├── fontello.woff2 │ ├── Lato-Italic.ttf │ ├── Lato-Regular.ttf │ ├── Lato-BoldItalic.ttf │ └── Montserrat-VariableFont_wght.ttf ├── doc │ ├── Editor-example.webp │ ├── Editor-example-2.webp │ ├── Cuvimaker-example.webp │ ├── Editor-example-light.webp │ ├── Cuvimaker-example-light.webp │ └── Editor-example-light-2.webp ├── assets │ └── close-icon.svg ├── favicon.svg ├── cuvimaker.svg └── cuvimakerText.svg ├── postcss.config.cjs ├── .vscode ├── extensions.json └── launch.json ├── tsconfig.json ├── .prettierr.config.js ├── .gitignore ├── .prettierrc ├── astro.config.mjs ├── .eslintrc.cjs ├── package.json ├── README.md └── tailwind.config.cjs /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /src/utils/eventBus.ts: -------------------------------------------------------------------------------- 1 | import mitt from 'mitt'; 2 | export default mitt(); 3 | -------------------------------------------------------------------------------- /public/cv.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/cv.webp -------------------------------------------------------------------------------- /public/cv2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/cv2.webp -------------------------------------------------------------------------------- /public/google0552f88e2da450de.html: -------------------------------------------------------------------------------- 1 | google-site-verification: google0552f88e2da450de.html 2 | -------------------------------------------------------------------------------- /public/project.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/project.webp -------------------------------------------------------------------------------- /src/store.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia'; 2 | export const appStore = createPinia(); 3 | -------------------------------------------------------------------------------- /public/fonts/fontello.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/fonts/fontello.ttf -------------------------------------------------------------------------------- /public/fonts/fontello.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/fonts/fontello.woff2 -------------------------------------------------------------------------------- /public/doc/Editor-example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/doc/Editor-example.webp -------------------------------------------------------------------------------- /public/fonts/Lato-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/fonts/Lato-Italic.ttf -------------------------------------------------------------------------------- /public/fonts/Lato-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/fonts/Lato-Regular.ttf -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/doc/Editor-example-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/doc/Editor-example-2.webp -------------------------------------------------------------------------------- /public/fonts/Lato-BoldItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/fonts/Lato-BoldItalic.ttf -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode"], 3 | "unwantedRecommendations": [] 4 | } 5 | -------------------------------------------------------------------------------- /public/doc/Cuvimaker-example.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/doc/Cuvimaker-example.webp -------------------------------------------------------------------------------- /public/doc/Editor-example-light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/doc/Editor-example-light.webp -------------------------------------------------------------------------------- /public/doc/Cuvimaker-example-light.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/doc/Cuvimaker-example-light.webp -------------------------------------------------------------------------------- /public/doc/Editor-example-light-2.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/doc/Editor-example-light-2.webp -------------------------------------------------------------------------------- /public/fonts/Montserrat-VariableFont_wght.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/carlosjorger/cuvimaker/HEAD/public/fonts/Montserrat-VariableFont_wght.ttf -------------------------------------------------------------------------------- /src/components/shared/Paragraph/CustomParagraph.astro: -------------------------------------------------------------------------------- 1 |

2 | 3 |

4 | -------------------------------------------------------------------------------- /src/components/CVExampleImages.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import CardImage from './CardImage.astro'; 3 | --- 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/components/NavigationLink.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { href, text = '' } = Astro.props; 3 | --- 4 | 5 | {text} 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/strict", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "allowJs": true 6 | }, 7 | "include": ["./src/*.ts", "**/*.vue"] 8 | } 9 | -------------------------------------------------------------------------------- /src/icons/trash.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/shared/Subsection/SubsectionAlign.vue: -------------------------------------------------------------------------------- 1 | 8 | -------------------------------------------------------------------------------- /src/models/SubsectionElement.ts: -------------------------------------------------------------------------------- 1 | export class SubsectionElement { 2 | public id: number; 3 | public name: string; 4 | constructor(id: number, name?: string) { 5 | this.id = id; 6 | this.name = name ?? ''; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/composables/useOpenModal.ts: -------------------------------------------------------------------------------- 1 | export function useOpenModal(modalId: string) { 2 | const onShowModal = () => { 3 | (document.getElementById(modalId) as HTMLDialogElement)?.showModal(); 4 | }; 5 | return { onShowModal }; 6 | } 7 | -------------------------------------------------------------------------------- /src/models/SubsectionTimeInterval.ts: -------------------------------------------------------------------------------- 1 | export class TimeInterval { 2 | dateFrom: Date | undefined; 3 | dateTo: Date | undefined; 4 | constructor(dateFrom?: Date, dateTo?: Date) { 5 | this.dateFrom = dateFrom; 6 | this.dateTo = dateTo; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /src/components/Title.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@fontsource/figtree'; 3 | import '@fontsource/figtree/900.css'; 4 | --- 5 | 6 |

9 | 10 |

11 | -------------------------------------------------------------------------------- /.prettierr.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | require('prettier-plugin-tailwindcss'), 4 | require.resolve('prettier-plugin-astro'), 5 | ], 6 | overrides: [ 7 | { 8 | files: '**/*.astro', 9 | options: { parser: 'astro' }, 10 | }, 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /src/components/Subtitle.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '@fontsource/figtree'; 3 | import '@fontsource/figtree/900.css'; 4 | --- 5 | 6 |

9 | 10 |

11 | -------------------------------------------------------------------------------- /src/utils/scrollServices.ts: -------------------------------------------------------------------------------- 1 | export const scrollSmoothToElement = (element: Element) => { 2 | setTimeout(() => { 3 | if (element instanceof Element) { 4 | element.scrollIntoView({ 5 | block: 'center', 6 | behavior: 'smooth', 7 | }); 8 | } 9 | }, 100); 10 | }; 11 | -------------------------------------------------------------------------------- /src/icons/edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | 4 | # dependencies 5 | node_modules/ 6 | 7 | # logs 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | pnpm-debug.log* 12 | 13 | 14 | # environment variables 15 | .env 16 | .env.production 17 | 18 | # macOS-specific files 19 | .DS_Store 20 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "useTabs": true, 4 | "tabWidth": 4, 5 | "singleQuote": true, 6 | "printWidth": 80, 7 | "htmlWhitespaceSensitivity": "css", 8 | "vueIndentScriptAndStyle": true, 9 | "plugins": ["prettier-plugin-astro", "prettier-plugin-tailwindcss"], 10 | "endOfLine": "lf" 11 | } 12 | -------------------------------------------------------------------------------- /src/components/shared/Anchor/BasicEmail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 14 | -------------------------------------------------------------------------------- /src/components/Navigation.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import NavigationLink from './NavigationLink.astro'; 3 | --- 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | type Theme = 'dark' | 'light'; 2 | 3 | function getTheme() { 4 | if (typeof localStorage !== 'undefined') { 5 | const theme = localStorage.getItem('theme'); 6 | if (theme) { 7 | return theme as Theme; 8 | } 9 | } 10 | } 11 | export function isDarkMode() { 12 | return getTheme() == 'dark'; 13 | } 14 | -------------------------------------------------------------------------------- /src/components/app/Preview/PreviewSectionHeader.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 15 | -------------------------------------------------------------------------------- /src/utils/resumePaths.ts: -------------------------------------------------------------------------------- 1 | export type ResumePathsType = { 2 | params: { 3 | id: string; 4 | }; 5 | }; 6 | export const getResumePaths = () => { 7 | return [ 8 | { params: { id: '1' } }, 9 | { params: { id: '2' } }, 10 | { params: { id: '3' } }, 11 | { params: { id: '4' } }, 12 | { params: { id: '5' } }, 13 | ]; 14 | }; 15 | -------------------------------------------------------------------------------- /src/components/shared/Tooltip/TooltipWrapper.vue: -------------------------------------------------------------------------------- 1 | 9 | 17 | -------------------------------------------------------------------------------- /src/models/Resume.ts: -------------------------------------------------------------------------------- 1 | import { Introduction } from './Introduction'; 2 | import type { Section } from './Section'; 3 | export class Resume { 4 | id: string; 5 | introduction: Introduction; 6 | sections: Section[]; 7 | constructor(id: string) { 8 | this.id = id; 9 | this.introduction = new Introduction(); 10 | this.sections = []; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/pages/editor.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from '../layouts/BaseLayout.astro'; 3 | import ResumesList from '../components/ResumesList.vue'; 4 | import Title from '../components/Title.astro'; 5 | --- 6 | 7 | 8 | Resumes 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Navigation from './Navigation.astro'; 3 | import ThemeIcon from './ThemeIcon.astro'; 4 | --- 5 | 6 |
7 | 15 |
16 | -------------------------------------------------------------------------------- /src/components/Social.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon'; 3 | const { platform, username, icon, extraPath = '' } = Astro.props; 4 | --- 5 | 6 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/components/shared/Button/ModalButton.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 20 | -------------------------------------------------------------------------------- /src/components/shared/Transition/AppearFadeTransition.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 17 | -------------------------------------------------------------------------------- /public/assets/close-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config'; 2 | import vue from '@astrojs/vue'; 3 | import tailwind from '@astrojs/tailwind'; 4 | import compressor from 'astro-compressor'; 5 | 6 | export default defineConfig({ 7 | integrations: [ 8 | vue(), 9 | tailwind({ 10 | config: { 11 | path: './tailwind.config.cjs', 12 | }, 13 | }), 14 | compressor({ gzip: true }), 15 | ], 16 | }); 17 | -------------------------------------------------------------------------------- /src/models/SocialAccount.ts: -------------------------------------------------------------------------------- 1 | import { getIconByUrl } from '../utils/urlService'; 2 | 3 | export class SocialAccount { 4 | public id: number; 5 | public link: string; 6 | get iconName(): string { 7 | return getIconByUrl(this.link); 8 | } 9 | constructor(id: number, name?: string) { 10 | this.id = id; 11 | this.link = name ?? ''; 12 | } 13 | copy() { 14 | return new SocialAccount(this.id, this.link); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line no-undef 2 | module.exports = { 3 | env: { 4 | browser: true, 5 | es2021: true, 6 | }, 7 | extends: [ 8 | 'eslint:recommended', 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:vue/vue3-essential', 11 | ], 12 | parserOptions: { 13 | ecmaVersion: 'latest', 14 | parser: '@typescript-eslint/parser', 15 | sourceType: 'module', 16 | }, 17 | plugins: ['@typescript-eslint', 'vue'], 18 | rules: {}, 19 | }; 20 | -------------------------------------------------------------------------------- /src/components/StaticImage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { id, imageSrc = '', width = 400, altText } = Astro.props; 3 | --- 4 | 5 | 10 | {altText} 16 | 17 | -------------------------------------------------------------------------------- /src/icons/linkedin.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/shared/Button/BasicButton.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 27 | -------------------------------------------------------------------------------- /src/components/shared/Transition/ListTransition.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | 13 | 28 | -------------------------------------------------------------------------------- /src/pages/editor/[id].astro: -------------------------------------------------------------------------------- 1 | --- 2 | import CVEditor from '../../components/CVEditor.vue'; 3 | import BaseLayout from '../../layouts/BaseLayout.astro'; 4 | import { getResumePaths } from '../../utils/resumePaths'; 5 | import '@vuepic/vue-datepicker/dist/main.css'; 6 | 7 | export async function getStaticPaths() { 8 | return getResumePaths(); 9 | } 10 | const { id } = Astro.params; 11 | --- 12 | 13 | 14 |
15 | 16 |
17 |
18 | -------------------------------------------------------------------------------- /src/components/shared/Transition/AppearFadePanelTransition.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 30 | -------------------------------------------------------------------------------- /src/icons/twitter.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/shared/Icon/IconByUrl.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | -------------------------------------------------------------------------------- /src/components/shared/others/ModalTemplate.vue: -------------------------------------------------------------------------------- 1 | 10 | 23 | -------------------------------------------------------------------------------- /src/icons/github.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/models/Section.ts: -------------------------------------------------------------------------------- 1 | import { Subsection } from './Subsection'; 2 | import '../extensions/extensions'; 3 | import { SectionTemplate } from './SectionTemplate'; 4 | export class Section { 5 | name: string; 6 | subsections: Subsection[]; 7 | sectionTemplate: SectionTemplate; 8 | constructor( 9 | name?: string, 10 | subsections?: Subsection[], 11 | sectionTemplate?: SectionTemplate 12 | ) { 13 | this.name = name ?? ''; 14 | this.subsections = subsections ?? [new Subsection(0)]; 15 | this.sectionTemplate = sectionTemplate ?? new SectionTemplate(); 16 | this.name = this.sectionTemplate.isFixedSectionName 17 | ? this.sectionTemplate.sectionTitleName 18 | : this.name; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/components/shared/Anchor/BasicLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 22 | 27 | -------------------------------------------------------------------------------- /src/components/shared/Error/ErrorsSection.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 28 | -------------------------------------------------------------------------------- /src/components/shared/checkbox/SwitchCheckbox.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 35 | -------------------------------------------------------------------------------- /src/components/shared/Button/IconButton.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 29 | 30 | -------------------------------------------------------------------------------- /src/components/Contacts.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Social from './Social.astro'; 3 | import Subtitle from './Subtitle.astro'; 4 | import CustomParagraph from './shared/Paragraph/CustomParagraph.astro'; 5 | --- 6 | 7 | Want to give me some feedback? 8 | You can contact me in these social media: 9 |
10 | 11 | 12 | 18 | 23 |
24 | -------------------------------------------------------------------------------- /src/models/Introduction.ts: -------------------------------------------------------------------------------- 1 | import { SocialAccount } from './SocialAccount'; 2 | 3 | export class Introduction { 4 | name = ''; 5 | profetion = ''; 6 | email: string | undefined; 7 | phone: string | undefined; 8 | location: string | undefined; 9 | website: string | undefined; 10 | socialAccounts: SocialAccount[]; 11 | socialAccountsCount = 0; 12 | constructor( 13 | name?: string, 14 | profetion?: string, 15 | email?: string, 16 | phone?: string, 17 | location?: string, 18 | 19 | website?: string, 20 | socialAccounts?: SocialAccount[] 21 | ) { 22 | this.name = name ?? ''; 23 | this.profetion = profetion ?? ''; 24 | this.email = email; 25 | this.phone = phone; 26 | this.location = location; 27 | this.website = website; 28 | this.socialAccounts = socialAccounts ?? []; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/components/shared/Button/CircleButtonWithIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 34 | -------------------------------------------------------------------------------- /src/layouts/TitleLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { Icon } from 'astro-icon'; 3 | import Subtitle from '../components/Subtitle.astro'; 4 | 5 | const pathname = Astro.url.pathname; 6 | const isANestedPathRex = /^\/\w+\/\w+/; 7 | const isANestedPath = pathname.match(isANestedPathRex)?.[0]; 8 | const parentPathRex = /^\/\w+/; 9 | const parentPath = pathname.match(parentPathRex)?.[0]; 10 | --- 11 | 12 |
13 | { 14 | isANestedPath && ( 15 | <> 16 | 21 | 22 | 23 | Go Back 24 | 25 | ) 26 | } 27 | 28 |
29 |
30 | -------------------------------------------------------------------------------- /src/components/shared/Button/CircleButton.vue: -------------------------------------------------------------------------------- 1 | 11 | 36 | -------------------------------------------------------------------------------- /src/components/shared/others/ShakeTemplate.vue: -------------------------------------------------------------------------------- 1 | 10 | 21 | 22 | 51 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/shared/Button/CloseAddButton.vue: -------------------------------------------------------------------------------- 1 | 21 | 44 | -------------------------------------------------------------------------------- /src/models/Subsection.ts: -------------------------------------------------------------------------------- 1 | import { SubsectionElement } from './SubsectionElement'; 2 | import type { TimeInterval } from './SubsectionTimeInterval'; 3 | 4 | export class Subsection { 5 | id: number; 6 | title: string; 7 | text: string; 8 | location: string; 9 | last: boolean; 10 | editing: boolean; 11 | subsectionTimeInterval: TimeInterval | undefined; 12 | elements: SubsectionElement[]; 13 | count: number; 14 | constructor(id?: number, title?: string, text?: string, location?: string) { 15 | this.id = id ?? 0; 16 | this.title = title ?? ''; 17 | this.text = text ?? ''; 18 | this.location = location ?? ''; 19 | this.last = true; 20 | this.editing = true; 21 | this.count = 0; 22 | this.elements = []; 23 | } 24 | get isEmpty(): boolean { 25 | return ( 26 | (this.title == '' || this.title == undefined) && 27 | (this.text == '' || this.text == undefined) 28 | ); 29 | } 30 | get hasElements(): boolean { 31 | return this.elements.length > 0; 32 | } 33 | addElement(elementName: string) { 34 | this.elements.push(new SubsectionElement(this.count++, elementName)); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/components/shared/TextArea/AdaptableTextArea.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 50 | -------------------------------------------------------------------------------- /src/styles/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --vdp-elem-font-size: 0.6em; 7 | --primary-color: #3c1774; 8 | --primary-form-color: #5726a1; 9 | --anchor-color: #42bbc2; 10 | --placeholder-color: rgb(180, 180, 180); 11 | --dark-primary-light: #51437a; 12 | --dark-primary: #3f3268; 13 | --dark-primary-200: #261c46; 14 | --dark-primary-300: #120a25; 15 | --dp-action-button-height: 30px !important; 16 | } 17 | 18 | @media (width <= 450px) { 19 | :root { 20 | font-size: 13px; 21 | } 22 | } 23 | ::-webkit-scrollbar { 24 | width: 10px; 25 | } 26 | 27 | ::-webkit-scrollbar-track { 28 | box-shadow: inset 0 0 5px grey; 29 | border-radius: 10px; 30 | } 31 | 32 | ::-webkit-scrollbar-thumb { 33 | background: oklch(var(--p)); 34 | border-radius: 10px; 35 | } 36 | 37 | .dark ::-webkit-scrollbar-thumb { 38 | background: oklch(var(--p)); 39 | border-radius: 10px; 40 | } 41 | 42 | ::-webkit-scrollbar-thumb:hover { 43 | background: oklch(var(--pf)); 44 | } 45 | 46 | .dark ::-webkit-scrollbar-thumb:hover { 47 | background: oklch(var(--pf)); 48 | border-radius: 10px; 49 | } 50 | -------------------------------------------------------------------------------- /src/components/app/Editor/EditorBar.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 47 | -------------------------------------------------------------------------------- /src/components/shared/Modal/ConfirmationModal.vue: -------------------------------------------------------------------------------- 1 | 26 | 44 | -------------------------------------------------------------------------------- /src/pages/index.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import BaseLayout from '../layouts/BaseLayout.astro'; 3 | import CVExampleImages from '../components/CVExampleImages.astro'; 4 | import Title from '../components/Title.astro'; 5 | import BasicButton from '../components/shared/Button/BasicButton.vue'; 6 | import CustomParagraph from '../components/shared/Paragraph/CustomParagraph.astro'; 7 | --- 8 | 9 | 10 |
11 | Create your own <br /> curriculum vitae 12 | 13 | Save your personal data, skills, experience, projects
and more. Download the cv in a pdf format. 16 |
17 | 22 |
23 | 24 | 28 | 29 |
30 |
31 | 34 |
35 | -------------------------------------------------------------------------------- /src/stores/ResumeStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Resume } from '../models/Resume'; 3 | import { Section } from '../models/Section'; 4 | import { copySection } from '../extensions/extensions'; 5 | type ResumeStoreStatus = { 6 | resume: Resume; 7 | isBeingEditingIntroduction: boolean; 8 | }; 9 | export const useResumeStore = defineStore('resume', { 10 | state: (): ResumeStoreStatus => ({ 11 | resume: new Resume('-1'), 12 | isBeingEditingIntroduction: false, 13 | }), 14 | getters: { 15 | anySectionWithThisName() { 16 | const sections = this.resume.sections; 17 | return (index: number | undefined, sectionName: string) => 18 | !sections.some((s, i) => i != index && s.name == sectionName); 19 | }, 20 | getSection() { 21 | return (index: number) => this.resume.sections[index]; 22 | }, 23 | }, 24 | actions: { 25 | deleteSection(index: number) { 26 | this.resume.sections.splice(index, 1); 27 | }, 28 | addSection(section: Section) { 29 | this.resume.sections.push(section); 30 | }, 31 | 32 | setSection(index: number, section: Section) { 33 | const sectionCopy = copySection(section); 34 | if (sectionCopy) { 35 | this.resume.sections[index] = sectionCopy; 36 | } 37 | }, 38 | setResume(resume: Resume) { 39 | this.resume = resume; 40 | }, 41 | }, 42 | }); 43 | -------------------------------------------------------------------------------- /src/stores/SubsectionStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Subsection } from '../models/Subsection'; 3 | import { SubsectionElement } from '../models/SubsectionElement'; 4 | import { copyElement, copyTimeInterval } from '../extensions/extensions'; 5 | 6 | type SubsectionStoreState = { 7 | subsection: Subsection; 8 | editing: boolean; 9 | elementsCount: number; 10 | selected: number; 11 | }; 12 | export const useSubsectionStore = defineStore('subsection', { 13 | state: (): SubsectionStoreState => ({ 14 | subsection: new Subsection(), 15 | editing: false, 16 | elementsCount: 0, 17 | selected: -1, 18 | }), 19 | actions: { 20 | addElement(elementName: string) { 21 | this.subsection.elements.push( 22 | new SubsectionElement(this.elementsCount++, elementName) 23 | ); 24 | }, 25 | setSubsection(subsection: Subsection) { 26 | this.subsection.title = subsection.title; 27 | this.subsection.text = subsection.text; 28 | this.subsection.last = subsection.last; 29 | this.subsection.editing = subsection.editing; 30 | this.subsection.count = subsection.count; 31 | 32 | this.subsection.subsectionTimeInterval = copyTimeInterval( 33 | subsection.subsectionTimeInterval 34 | ); 35 | 36 | this.subsection.elements = subsection.elements.map((element) => 37 | copyElement(element) 38 | ); 39 | }, 40 | }, 41 | }); 42 | -------------------------------------------------------------------------------- /src/pages/about.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Contacts from '../components/Contacts.astro'; 3 | import BaseLayout from '../layouts/BaseLayout.astro'; 4 | import Subtitle from '../components/Subtitle.astro'; 5 | import StaticImage from '../components/StaticImage.astro'; 6 | import CustomParagraph from '../components/shared/Paragraph/CustomParagraph.astro'; 7 | 8 | const platform = 'github'; 9 | const username = 'carlosjorger'; 10 | const proyect = 'astro-vue-online-cv-maker'; 11 | --- 12 | 13 | 14 |
15 | 18 | Are you interested in this app? 19 | 20 | Learn more about this project on 21 | 25 | {platform}. 27 |
28 | Contributions are welcome too!! 29 |
30 | 31 |
32 | 35 |
36 | -------------------------------------------------------------------------------- /src/utils/urlService.ts: -------------------------------------------------------------------------------- 1 | export const getInfoFromUrl = (url: string) => { 2 | const urlWithoutHttps = url.replace('https://', ''); 3 | const urlWithoutHttp = urlWithoutHttps.replace('http://', ''); 4 | const urlWithoutWWW = urlWithoutHttp.replace('www.', ''); 5 | const urlWithoutMailto = urlWithoutWWW.replace('mailto:', ''); 6 | if (urlWithoutMailto.endsWith('/')) { 7 | return urlWithoutMailto.slice(0, -1); 8 | } 9 | return urlWithoutMailto; 10 | }; 11 | 12 | export const getIconByUrl = (url: string) => { 13 | const socialMediaDict = new Map(); 14 | socialMediaDict.set('github.com/', 'mdi:github'); 15 | socialMediaDict.set('twitter.com/', 'mdi:twitter'); 16 | socialMediaDict.set('linkedin.com/in/', 'mdi:linkedin'); 17 | socialMediaDict.set('reddit.com', 'ic:baseline-reddit'); 18 | socialMediaDict.set('t.me', 'ic:baseline-telegram'); 19 | socialMediaDict.set('instagram.com', 'mdi:instagram'); 20 | socialMediaDict.set('facebook.com', 'ic:baseline-facebook'); 21 | socialMediaDict.set('tiktok.com', 'ic:baseline-tiktok'); 22 | socialMediaDict.set('youtube.com', 'mdi:youtube'); 23 | socialMediaDict.set('twitch.tv', 'mdi:twitch'); 24 | 25 | const keys = [...socialMediaDict.keys()]; 26 | for (let index = 0; index < keys.length; index++) { 27 | const key = keys[index]; 28 | if (url.includes(key)) { 29 | return socialMediaDict.get(key) ?? ''; 30 | } 31 | } 32 | return 'bi:person-fill'; 33 | }; 34 | -------------------------------------------------------------------------------- /public/cuvimaker.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 11 | 12 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/app/Preview/PreviewSection.vue: -------------------------------------------------------------------------------- 1 | 2 | 25 | 26 | 48 | -------------------------------------------------------------------------------- /src/components/app/Editor/List/EditorListSection.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 44 | -------------------------------------------------------------------------------- /src/composables/useDrag.ts: -------------------------------------------------------------------------------- 1 | import { ref } from 'vue'; 2 | export function useDrag(typeOfDrag: string, save: () => void) { 3 | const markedSection = ref(-1); 4 | const IdAttr = `${typeOfDrag}ID`; 5 | 6 | const onDragEnter = (index: number, enableDrag: boolean) => { 7 | if (!enableDrag) { 8 | return; 9 | } 10 | markedSection.value = index; 11 | }; 12 | const onDrop = ( 13 | event: DragEvent, 14 | element: T, 15 | collection: T[], 16 | index: number, 17 | enableDrag: boolean 18 | ) => { 19 | if (!enableDrag) { 20 | return; 21 | } 22 | const eventDataTransfer = event.dataTransfer; 23 | if (eventDataTransfer) { 24 | const draggedSectionName = eventDataTransfer.getData(IdAttr); 25 | const sourceIndex = Number.parseInt(draggedSectionName); 26 | const sourceSection = collection[sourceIndex]; 27 | collection[sourceIndex] = element; 28 | collection[index] = sourceSection; 29 | markedSection.value = -1; 30 | save(); 31 | } 32 | }; 33 | const startDrag = (event: DragEvent, index: number) => { 34 | event.stopPropagation(); 35 | const eventDataTransfer = event.dataTransfer; 36 | if (eventDataTransfer) { 37 | eventDataTransfer.dropEffect = 'move'; 38 | eventDataTransfer.effectAllowed = 'move'; 39 | eventDataTransfer.setData(IdAttr, index.toString()); 40 | } 41 | (event.target as HTMLElement).style.opacity = '1'; 42 | }; 43 | return { onDragEnter, onDrop, startDrag, markedSection }; 44 | } 45 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@example/minimal", 3 | "type": "module", 4 | "version": "0.0.1", 5 | "private": true, 6 | "scripts": { 7 | "dev": "astro dev", 8 | "start": "astro dev", 9 | "build": "astro build", 10 | "preview": "astro preview", 11 | "astro": "astro", 12 | "test": "vitest" 13 | }, 14 | "dependencies": { 15 | "@astrojs/tailwind": "^5.0.2", 16 | "@astrojs/vue": "^3.0.4", 17 | "@fontsource/black-ops-one": "^5.0.8", 18 | "@fontsource/figtree": "^5.0.17", 19 | "@fontsource/montserrat": "^5.0.15", 20 | "@fortawesome/fontawesome-svg-core": "^6.4.2", 21 | "@types/node": "^20.6.0", 22 | "@vitejs/plugin-vue": "^4.5.0", 23 | "@vuelidate/core": "^2.0.3", 24 | "@vuelidate/validators": "^2.0.4", 25 | "@vuepic/vue-datepicker": "^7.2.2", 26 | "astro": "^3.5.7", 27 | "astro-compressor": "^0.4.1", 28 | "astro-icon": "^0.8.1", 29 | "jsdom": "^22.1.0", 30 | "mitt": "^3.0.1", 31 | "pdfmake": "^0.2.8", 32 | "pinia": "^2.1.7", 33 | "typescript": "^5.3.2", 34 | "vue": "^3.3.8" 35 | }, 36 | "devDependencies": { 37 | "@iconify/vue": "^4.1.1", 38 | "@types/pdfmake": "^0.2.8", 39 | "@typescript-eslint/eslint-plugin": "^5.59.9", 40 | "@typescript-eslint/parser": "^6.12.0", 41 | "autoprefixer": "^10.4.16", 42 | "daisyui": "^4.4.2", 43 | "eslint": "^8.54.0", 44 | "eslint-plugin-vue": "^9.18.1", 45 | "postcss": "^8.4.31", 46 | "prettier": "^3.1.0", 47 | "prettier-plugin-astro": "^0.12.2", 48 | "prettier-plugin-tailwindcss": "^0.5.7", 49 | "tailwindcss": "^3.3.5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/components/CVEditor.vue: -------------------------------------------------------------------------------- 1 | 14 | 40 | -------------------------------------------------------------------------------- /src/components/shared/TextArea/BasicTextArea.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 64 | -------------------------------------------------------------------------------- /public/cuvimakerText.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 15 | 16 | 21 | 22 | 23 | 24 | 25 | CUVI 26 | CUVI 27 | MAKER 28 | MAKER 29 | 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Icon 3 |
Cuvimaker
4 |

5 | An online cv maker web application used to design a custom curriculum vitae and save it in a pdf file. 6 | 7 | | Dark Mode 🌙 | Light mode 🌞 | 8 | | :-------------------------------------------------------: | :-------------------------------------------------------------------: | 9 | | ![Cuvimaker Example](./public/doc/Cuvimaker-example.webp) | ![Cuvimaker Example Light](./public/doc/Cuvimaker-example-light.webp) | 10 | | ![Editor Example](./public/doc/Editor-example.webp) | ![Editor Example2](./public/doc/Editor-example-light.webp) | 11 | | ![Editor Example](./public/doc/Editor-example-2.webp) | ![Editor Example2](./public/doc/Editor-example-light-2.webp) | 12 | 13 | ## 🚀 Demo 14 | 15 | View a live demo of [Cuvimaker](https://cuvimaker.netlify.app/) 16 | 17 | ## 🧰 Features 18 | 19 | - Versatile creation of CVs. 20 | - Light/dark mode. 21 | - Local Storage data persistence. 22 | - Responsive Design. 23 | - Download CV in PDF format. 24 | - Save at most 6 CVs. 25 | 26 | ## 🧞 Commands 27 | 28 | All commands are run from the root of the project, from a terminal: 29 | 30 | | Command | Action | 31 | | :---------------- | :------------------------------------------- | 32 | | `npm install` | Installs dependencies | 33 | | `npm run dev` | Starts local dev server at `localhost:4321` | 34 | | `npm run build` | Build your production site to `./dist/` | 35 | | `npm run preview` | Preview your build locally, before deploying | 36 | 37 | ## :iphone:Technologies 38 | 39 | - Astro 40 | - Vue3 41 | - Tailwind 42 | - Daisyui 43 | -------------------------------------------------------------------------------- /tailwind.config.cjs: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | /*eslint-env node*/ 3 | module.exports = { 4 | content: ['./index.html', './src/**/*.{vue,js,ts,jsx,tsx,astro}'], 5 | darkMode: 'class', 6 | theme: { 7 | extend: { 8 | backgroundImage: { 9 | anchorImage: 10 | 'linear-gradient(var(--anchor-color),var(--anchor-color))', 11 | primaryImage: 12 | 'linear-gradient(var(--primary-color),var(--primary-color))', 13 | zincImage: 'linear-gradient(rgb(244 244 245),rgb(244 244 245))', 14 | }, 15 | colors: { 16 | anchor: 'var(--anchor-color)', 17 | ['dark-primary-light']: '#51437a', 18 | ['dark-primary-200']: '#261c46', 19 | ['dark-primary-300']: '#120a25', 20 | }, 21 | transitionProperty: { 22 | form: 'border-bottom, box-shadow', 23 | }, 24 | translate: ['dark'], 25 | keyframes: { 26 | cardImageIn: { 27 | '0%': { 28 | transform: 'rotateX(15deg) rotateY(-15deg)', 29 | 'box-shadow': '15px 15px 45px var(--tw-shadow-color)', 30 | }, 31 | '25%': { 32 | transform: 'rotateX(-11deg) rotateY(-5deg)', 33 | 'box-shadow': '5px -11px 38px var(--tw-shadow-color)', 34 | }, 35 | '50%': { 36 | transform: 'rotateX(4deg) rotateY(7deg)', 37 | 'box-shadow': '-7px 4px 35.5px var(--tw-shadow-color)', 38 | }, 39 | '75%': { 40 | transform: 'rotateX(2deg) rotateY(-3deg)', 41 | 'box-shadow': '3px 2px 33px var(--tw-shadow-color)', 42 | }, 43 | '100%': { 44 | transform: 'rotateX(0deg) rotateY(0deg)', 45 | 'box-shadow': '0px 0px 30px var(--tw-shadow-color)', 46 | }, 47 | }, 48 | }, 49 | animation: { 50 | cardImageIn: 'cardImageIn 2s ease-in-out', 51 | }, 52 | }, 53 | fontFamily: { 54 | montserrat: ['Montserrat', 'ui-sans-serif'], 55 | figtree: ['Figtree', 'sans-serif'], 56 | blackOpsOne: ['"Black Ops One"'], 57 | }, 58 | }, 59 | daisyui: { 60 | themes: ['light', 'dark'], 61 | }, 62 | plugins: [require('daisyui')], 63 | }; 64 | -------------------------------------------------------------------------------- /src/stores/SectionStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Section } from '../models/Section'; 3 | import { Subsection } from '../models/Subsection'; 4 | import { copySection } from '../extensions/extensions'; 5 | import type { SectionTemplate } from '../models/SectionTemplate'; 6 | type SectionStoreState = { 7 | section: Section; 8 | editingIndex: number; 9 | count: number; 10 | }; 11 | export const useSectionStore = defineStore('section', { 12 | state: (): SectionStoreState => ({ 13 | section: new Section(), 14 | editingIndex: -1, 15 | count: 0, 16 | }), 17 | getters: { 18 | subsectionEditing(): boolean { 19 | return this.editingIndex != -1; 20 | }, 21 | lastSubsectionIndex(): number { 22 | return this.section.subsections.length - 1; 23 | }, 24 | }, 25 | actions: { 26 | setSection(section: Section) { 27 | const sectionCopy = copySection(section); 28 | if (sectionCopy) { 29 | this.section = sectionCopy; 30 | } 31 | }, 32 | clear() { 33 | this.editingIndex = -1; 34 | this.section = new Section(); 35 | }, 36 | disabledEditing() { 37 | this.editingIndex = -1; 38 | }, 39 | setEditingIndex(index: number) { 40 | this.editingIndex = index; 41 | }, 42 | addNewSubsection() { 43 | this.section.subsections[this.lastSubsectionIndex].last = false; 44 | this.count++; 45 | this.editingIndex = this.lastSubsectionIndex; 46 | this.section.subsections.push(new Subsection(this.count)); 47 | }, 48 | setSubsection(index: number, subsection: Subsection) { 49 | this.section.subsections[index] = subsection; 50 | }, 51 | setTemplate(template: SectionTemplate) { 52 | this.section.sectionTemplate = template; 53 | if (template.isFixedSectionName) { 54 | this.section.name = template.sectionTitleName; 55 | } 56 | }, 57 | removeSubsection(index: number) { 58 | if (this.editingIndex == index) { 59 | this.disabledEditing(); 60 | } else if (this.editingIndex > index) { 61 | this.editingIndex--; 62 | } 63 | this.section.subsections.splice(index, 1); 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /src/components/app/Editor/TimeInterval/EditorSiteSection.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 66 | -------------------------------------------------------------------------------- /src/models/SectionTemplate.ts: -------------------------------------------------------------------------------- 1 | export class SectionTemplate { 2 | name: string; 3 | sectionTitleName: string; 4 | subsectionTitleName: string; 5 | subtitleName: string; 6 | isEnableSubtitle: boolean; 7 | isEnableSite: boolean; 8 | isEnableList: boolean; 9 | subsectionElement: string; 10 | subsectionElementSection: string; 11 | isFixedSectionName: boolean; 12 | subsectionName: string; 13 | constructor( 14 | name = 'Custom', 15 | sectionTitleName = 'Section Name', 16 | subsectionTitleName = 'Title', 17 | subtitleName = 'Subtitle', 18 | isEnableSubtitle = true, 19 | isEnableSite = true, 20 | isEnableList = true, 21 | subsectionElementSection = 'element section', 22 | subsectionElement = 'element', 23 | isFixedSectionName = false, 24 | subsectionName = 'Subsection' 25 | ) { 26 | this.name = name; 27 | this.sectionTitleName = sectionTitleName; 28 | this.subsectionTitleName = subsectionTitleName; 29 | this.subtitleName = subtitleName; 30 | this.isEnableSubtitle = isEnableSubtitle; 31 | this.isEnableSite = isEnableSite; 32 | this.isEnableList = isEnableList; 33 | this.subsectionElementSection = subsectionElementSection; 34 | this.subsectionElement = subsectionElement; 35 | this.isFixedSectionName = isFixedSectionName; 36 | this.subsectionName = subsectionName; 37 | } 38 | } 39 | export const sectionTemplates = [ 40 | new SectionTemplate(), 41 | new SectionTemplate( 42 | 'Work Experience', 43 | 'Work Experience', 44 | 'Company name', 45 | 'Position', 46 | true, 47 | true, 48 | true, 49 | 'description section', 50 | 'description', 51 | true, 52 | 'work' 53 | ), 54 | new SectionTemplate( 55 | 'Projects', 56 | 'Projects', 57 | 'Project name', 58 | 'Position', 59 | false, 60 | true, 61 | true, 62 | 'description section', 63 | 'description', 64 | true, 65 | 'project' 66 | ), 67 | new SectionTemplate( 68 | 'Skills', 69 | 'Skills', 70 | 'Skill name', 71 | 'Position', 72 | false, 73 | false, 74 | false, 75 | 'description section', 76 | 'description', 77 | true, 78 | 'skill' 79 | ), 80 | ]; 81 | -------------------------------------------------------------------------------- /src/utils/localStorage.ts: -------------------------------------------------------------------------------- 1 | import { Resume } from '../models/Resume'; 2 | 3 | function ReviveDateTime(key: unknown, value: unknown): unknown { 4 | if (typeof value === 'string') { 5 | const date = 6 | /^(?:\d{4})-(?:\d{2})-(?:\d{2})T(?:\d{2}):(?:\d{2}):(?:\d{2}(?:\.\d*)?)(?:(?:-(?:\d{2}):(?:\d{2})|Z)?)$/.exec( 7 | value 8 | ); 9 | if (date) { 10 | return new Date(date[0]); 11 | } 12 | } 13 | 14 | return value; 15 | } 16 | 17 | const loadResumes = (): Resume[] => { 18 | const resumesStringFormat = window.localStorage.getItem('resumes'); 19 | try { 20 | const resumes = JSON.parse(resumesStringFormat ?? '{}', ReviveDateTime); 21 | if (resumes !== '') { 22 | return Object.assign([new Resume('-1')], resumes) as [Resume]; 23 | } 24 | } catch (error) { 25 | if (typeof error === 'string') { 26 | console.log('Error: ', error.toUpperCase()); 27 | } else if (error instanceof Error) { 28 | console.log('Error: ', error.message); 29 | } 30 | } 31 | return [] as Resume[]; 32 | }; 33 | export const getResume = (id: string) => { 34 | const resumes = loadResumes(); 35 | return resumes.find((resume) => resume.id === id); 36 | }; 37 | export const clearResume = (id: string) => { 38 | const newResume = new Resume(id); 39 | const resumes = loadResumes(); 40 | const resumeIndex = resumes.findIndex((r) => r.id === id); 41 | if (resumeIndex >= 0) { 42 | resumes[resumeIndex] = newResume; 43 | } else { 44 | resumes.push(newResume); 45 | } 46 | window.localStorage.setItem('resumes', JSON.stringify(resumes)); 47 | }; 48 | export const loadResume = (id: string) => { 49 | const resume = new Resume(id); 50 | const resumes = loadResumes(); 51 | return resumes.find((resume) => resume.id === id) ?? resume; 52 | }; 53 | export const saveResumeInLocalStorage = (resume: Resume) => { 54 | const resumes = loadResumes(); 55 | const resumeIndex = resumes.findIndex((r) => r.id === resume.id); 56 | if (resumeIndex >= 0) { 57 | resumes[resumeIndex] = resume; 58 | } else { 59 | resumes.push(resume); 60 | } 61 | window.localStorage.setItem('resumes', JSON.stringify(resumes)); 62 | }; 63 | -------------------------------------------------------------------------------- /src/components/ResumeElement.vue: -------------------------------------------------------------------------------- 1 | 30 | 67 | -------------------------------------------------------------------------------- /src/extensions/extensions.ts: -------------------------------------------------------------------------------- 1 | import type { Introduction } from '../models/Introduction'; 2 | import { Section } from '../models/Section'; 3 | import { Subsection } from '../models/Subsection'; 4 | import { SubsectionElement } from '../models/SubsectionElement'; 5 | import { TimeInterval } from '../models/SubsectionTimeInterval'; 6 | 7 | export function copySection(section?: Section) { 8 | if (section) { 9 | return new Section( 10 | section.name, 11 | section.subsections?.map((s) => copySubsection(s)) ?? [ 12 | new Subsection(0), 13 | ], 14 | section.sectionTemplate 15 | ); 16 | } 17 | } 18 | export function copySubsection(subsection: Subsection) { 19 | const result = new Subsection( 20 | subsection.id, 21 | subsection.title, 22 | subsection.text, 23 | subsection.location 24 | ); 25 | result.last = subsection.last; 26 | result.editing = subsection.editing; 27 | result.count = subsection.count; 28 | result.subsectionTimeInterval = copyTimeInterval( 29 | subsection.subsectionTimeInterval 30 | ); 31 | result.elements = subsection.elements.map((element) => 32 | copyElement(element) 33 | ); 34 | return result; 35 | } 36 | export function copyTimeInterval(subsectionTimeInterval?: TimeInterval) { 37 | if (subsectionTimeInterval) { 38 | return new TimeInterval( 39 | subsectionTimeInterval.dateFrom, 40 | subsectionTimeInterval.dateTo 41 | ); 42 | } 43 | } 44 | export function copyElement(subsectionElement: SubsectionElement) { 45 | return new SubsectionElement(subsectionElement.id, subsectionElement.name); 46 | } 47 | export function isEmptySubsection(subsection: Subsection) { 48 | return ( 49 | (subsection.title == '' || subsection.title == undefined) && 50 | (subsection.text == '' || subsection.text == undefined) 51 | ); 52 | } 53 | export function isNotEmptyIntroduction(introduction: Introduction) { 54 | return Boolean( 55 | introduction.email || 56 | introduction.location || 57 | introduction.name.length || 58 | introduction.phone || 59 | introduction.profetion.length || 60 | introduction.socialAccounts.length || 61 | introduction.website 62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /src/components/app/Preview/PreviewResume.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 67 | -------------------------------------------------------------------------------- /src/stores/IntroductionStore.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia'; 2 | import { Introduction } from '../models/Introduction'; 3 | import { SocialAccount } from '../models/SocialAccount'; 4 | type IntroductionStoreState = { 5 | introduction: Introduction; 6 | editing: boolean; 7 | socialAccountsCount: number; 8 | selected: number; 9 | }; 10 | export const useIntroductionStore = defineStore('introduction', { 11 | state: (): IntroductionStoreState => ({ 12 | introduction: new Introduction(), 13 | editing: false, 14 | socialAccountsCount: 0, 15 | selected: -1, 16 | }), 17 | getters: { 18 | isSelected({ selected }): (SocialAccountId: number) => boolean { 19 | return (SocialAccountId: number): boolean => 20 | selected == SocialAccountId; 21 | }, 22 | copy: (state) => 23 | new Introduction( 24 | state.introduction.name, 25 | state.introduction.profetion, 26 | state.introduction.email, 27 | state.introduction.phone, 28 | state.introduction.location, 29 | state.introduction.website, 30 | state.introduction.socialAccounts 31 | ), 32 | }, 33 | actions: { 34 | addSocialAccount(link: string) { 35 | this.socialAccountsCount = this.introduction.socialAccounts.length; 36 | this.introduction.socialAccounts.push( 37 | new SocialAccount(this.socialAccountsCount, link) 38 | ); 39 | }, 40 | selectASocialAccount(Id: number) { 41 | this.selected = Id; 42 | }, 43 | unSelectASocialAccount(Id: number) { 44 | if (this.selected == Id) { 45 | this.selected = -1; 46 | } 47 | }, 48 | removeSocialAccount(index: number) { 49 | this.introduction.socialAccounts.splice(index, 1); 50 | }, 51 | saveSocialAccount(index: number, link: string) { 52 | this.introduction.socialAccounts[index].link = link; 53 | }, 54 | setIntroduction(introduction: Introduction) { 55 | this.introduction = new Introduction( 56 | introduction.name, 57 | introduction.profetion, 58 | introduction.email, 59 | introduction.phone, 60 | introduction.location, 61 | introduction.website, 62 | introduction.socialAccounts 63 | ); 64 | }, 65 | }, 66 | }); 67 | -------------------------------------------------------------------------------- /src/components/app/Preview/PreviewIntroduction.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 74 | -------------------------------------------------------------------------------- /src/components/CardImage.astro: -------------------------------------------------------------------------------- 1 | --- 2 | const { id, imageSrc = '', width = '22rem', altText } = Astro.props; 3 | --- 4 | 5 |
9 | 13 | {altText} 14 | 15 |
16 | 55 | 60 | -------------------------------------------------------------------------------- /src/components/shared/Subsection/SubsectionCard.vue: -------------------------------------------------------------------------------- 1 | 25 | 68 | -------------------------------------------------------------------------------- /src/components/ResumesList.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 67 | -------------------------------------------------------------------------------- /src/components/app/Preview/PreviewSubsection.vue: -------------------------------------------------------------------------------- 1 | 74 | 75 | 86 | -------------------------------------------------------------------------------- /src/components/app/Editor/Section/EditorSection.vue: -------------------------------------------------------------------------------- 1 | 49 | 88 | -------------------------------------------------------------------------------- /src/layouts/BaseLayout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import '../styles/global.css'; 3 | import Header from '../components/Header.astro'; 4 | import '@fontsource/montserrat'; 5 | import ViewTransitions from 'astro/components/ViewTransitions.astro'; 6 | import TitleLayout from './TitleLayout.astro'; 7 | 8 | interface Props { 9 | pageTitle: string; 10 | } 11 | const { pageTitle } = Astro.props; 12 | const pageDescription = 13 | 'An online editor of curriculum vitaes 🧑‍💼, SEO-friendly made in Astro🚀'; 14 | const isEditPage = pageTitle.includes('Editor'); 15 | --- 16 | 17 | 18 | 19 | 20 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 50 | 51 | {pageTitle} 52 | 53 | 54 | 57 |
58 |
59 | { 60 | !isEditPage && ( 61 |
62 | 63 |
64 | ) 65 | } 66 | { 67 | isEditPage && ( 68 |
69 | 70 | 71 |
72 | ) 73 | } 74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /src/utils/pdf/resumePDF.ts: -------------------------------------------------------------------------------- 1 | import type { TDocumentDefinitions } from 'pdfmake/interfaces'; 2 | import type { Resume } from '../../models/Resume'; 3 | import type { Section } from '../../models/Section'; 4 | import { createIntroductionDefinition } from './introductionPDF'; 5 | import { creatreSectionDefinition } from './sectionPDF'; 6 | import { latoBold } from '../../../public/fonts/Lato'; 7 | export function createResumePDFDefinition( 8 | resume: Resume 9 | ): TDocumentDefinitions { 10 | return { 11 | content: [ 12 | createIntroductionDefinition(resume.introduction), 13 | ...createSectionsDefinition(resume.sections), 14 | ], 15 | styles: { 16 | h1: { 17 | fontSize: 30, 18 | bold: true, 19 | font: 'Lato', 20 | }, 21 | h2: { 22 | marginTop: 4, 23 | fontSize: 22, 24 | bold: true, 25 | font: 'Lato', 26 | }, 27 | h3: { 28 | fontSize: 17, 29 | marginTop: 6, 30 | }, 31 | h4: { 32 | fontSize: 14, 33 | marginTop: 6, 34 | bold: true, 35 | font: 'Lato', 36 | }, 37 | h5: { 38 | fontSize: 12, 39 | marginTop: 2, 40 | }, 41 | h6: { 42 | marginTop: 5, 43 | }, 44 | introductionColumn: { 45 | fontSize: 10, 46 | marginTop: 2, 47 | marginBottom: 2, 48 | }, 49 | link: { 50 | color: 'blue', 51 | }, 52 | br: { 53 | background: 'black', 54 | }, 55 | ul: { 56 | marginTop: 2, 57 | }, 58 | subsection: { 59 | marginTop: 8, 60 | marginBottom: 8, 61 | }, 62 | introduction: { 63 | marginBottom: 12, 64 | }, 65 | introductionRow: { 66 | marginTop: 3, 67 | }, 68 | introductionColumnsBlock: { 69 | marginTop: 5, 70 | }, 71 | timeInterval: { 72 | marginTop: 6, 73 | }, 74 | location: { 75 | marginTop: 5, 76 | }, 77 | }, 78 | }; 79 | } 80 | function createSectionsDefinition(sections: Section[]) { 81 | return sections.map((section) => creatreSectionDefinition(section)); 82 | } 83 | 84 | export async function savePDF(resumeDefinition: TDocumentDefinitions) { 85 | const { createPdf } = await import('pdfmake/build/pdfmake.min'); 86 | const pdfFonts = await import('pdfmake/build/vfs_fonts'); 87 | 88 | const vsf = 89 | pdfFonts && pdfFonts.pdfMake 90 | ? pdfFonts.pdfMake.vfs 91 | : globalThis.pdfMake.vfs; 92 | vsf['Lato-Bold.tff'] = latoBold; 93 | const fonts = { 94 | Roboto: { 95 | normal: 'Roboto-Regular.ttf', 96 | bold: 'Roboto-Medium.ttf', 97 | italics: 'Roboto-Italic.ttf', 98 | bolditalics: 'Roboto-MediumItalic.ttf', 99 | }, 100 | Lato: { 101 | bold: 'Lato-Bold.tff', 102 | }, 103 | }; 104 | const pdf = createPdf(resumeDefinition, undefined, fonts, vsf); 105 | pdf.open(); 106 | } 107 | -------------------------------------------------------------------------------- /src/components/ThemeIcon.astro: -------------------------------------------------------------------------------- 1 | --- 2 | 3 | --- 4 | 5 | 31 | 68 | -------------------------------------------------------------------------------- /src/components/app/Editor/List/EditorElements.vue: -------------------------------------------------------------------------------- 1 | 57 | 58 | 120 | -------------------------------------------------------------------------------- /src/utils/pdf/introductionPDF.ts: -------------------------------------------------------------------------------- 1 | import type { Column, Content, ContentColumns } from 'pdfmake/interfaces'; 2 | import { getInfoFromUrl } from '../urlService'; 3 | import { getIconByUrl } from '../urlService'; 4 | import type { Introduction } from '../../models/Introduction'; 5 | 6 | const enum IntroductionColumnType { 7 | email, 8 | link, 9 | text, 10 | } 11 | 12 | export function createIntroductionDefinition( 13 | introduction: Introduction 14 | ): Column { 15 | return { 16 | stack: [ 17 | { text: introduction.name.toLocaleUpperCase(), style: 'h1' }, 18 | { text: introduction.profetion, style: 'h2' }, 19 | createIntroductionColumnsDefinition(introduction), 20 | ], 21 | style: ['introduction'], 22 | }; 23 | } 24 | function createIntroductionColumnsDefinition( 25 | introduction: Introduction 26 | ): Content { 27 | return { 28 | stack: createIntroductionColumns(introduction), 29 | style: ['introductionColumnsBlock'], 30 | }; 31 | } 32 | function createIntroductionColumns(introduction: Introduction) { 33 | const columns = [] as Content[]; 34 | let count = 0; 35 | if (introduction.location) { 36 | addIntroductionColumn( 37 | columns, 38 | introduction.location, 39 | count++, 40 | IntroductionColumnType.text, 41 | 'location' 42 | ); 43 | } 44 | if (introduction.email) { 45 | addIntroductionColumn( 46 | columns, 47 | introduction.email, 48 | count++, 49 | IntroductionColumnType.email, 50 | 'email' 51 | ); 52 | } 53 | if (introduction.website) { 54 | addIntroductionColumn( 55 | columns, 56 | introduction.website, 57 | count++, 58 | IntroductionColumnType.link, 59 | 'website' 60 | ); 61 | } 62 | for (let index = 0; index < introduction.socialAccounts.length; index++) { 63 | const element = introduction.socialAccounts[index]; 64 | addIntroductionColumn( 65 | columns, 66 | element.link, 67 | count++, 68 | IntroductionColumnType.link, 69 | getIconByUrl(element.link) 70 | ); 71 | } 72 | return columns; 73 | } 74 | function addIntroductionColumn( 75 | result: Content[], 76 | text: string, 77 | count: number, 78 | introductionColumnType: IntroductionColumnType, 79 | iconType?: string 80 | ) { 81 | const svgColumn = getSocialAccountSvgIconColumn(iconType); 82 | const column = createIntroductionColumn(text, introductionColumnType); 83 | let currentColumns = [column]; 84 | if (svgColumn) { 85 | currentColumns = [svgColumn, ...currentColumns]; 86 | } 87 | const lastColumn: ContentColumns = { 88 | columns: currentColumns, 89 | }; 90 | if (count % 3 == 0) { 91 | result.push({ 92 | columns: [lastColumn], 93 | style: ['introductionRow'], 94 | }); 95 | } else { 96 | const lastColumns = result.pop(); 97 | if (isContentColumns(lastColumns)) { 98 | const columns = lastColumns as ContentColumns; 99 | result.push({ 100 | columns: [...columns.columns, lastColumn], 101 | style: ['introductionRow'], 102 | }); 103 | } 104 | } 105 | } 106 | const isContentColumns = ( 107 | content: Content | undefined 108 | ): content is ContentColumns => { 109 | return Boolean(content); 110 | }; 111 | function createIntroductionColumn( 112 | link: string, 113 | introductionColumnType: IntroductionColumnType 114 | ): Column { 115 | const urlInfo = getInfoFromUrl(link); 116 | if (introductionColumnType == IntroductionColumnType.link) { 117 | return { 118 | text: urlInfo, 119 | link: link, 120 | style: ['introductionColumn', 'link'], 121 | }; 122 | } else if (introductionColumnType == IntroductionColumnType.email) { 123 | return { 124 | text: urlInfo, 125 | link: `mailto:${link}`, 126 | style: ['introductionColumn', 'link'], 127 | }; 128 | } else { 129 | return { text: urlInfo, style: 'introductionColumn' }; 130 | } 131 | } 132 | function getSocialAccountSvgIconColumn(iconType?: string): Column | undefined { 133 | const svgElement = iconType ? document.getElementById(iconType) : undefined; 134 | if (svgElement && svgElement instanceof SVGSVGElement) { 135 | svgElement.setAttribute('width', '15'); 136 | svgElement.setAttribute('height', '15'); 137 | } 138 | const svg = iconType 139 | ? document.getElementById(iconType)?.outerHTML 140 | : undefined; 141 | if (svg) { 142 | return getSVGIconColunm(svg); 143 | } 144 | } 145 | function getSVGIconColunm(svg: string): Column { 146 | return { 147 | columns: [ 148 | { 149 | svg: svg, 150 | style: ['html-svg'], 151 | }, 152 | ], 153 | width: 20, 154 | }; 155 | } 156 | -------------------------------------------------------------------------------- /src/utils/pdf/sectionPDF.ts: -------------------------------------------------------------------------------- 1 | import type { Column, Content } from 'pdfmake/interfaces'; 2 | 3 | import type { Section } from '../../models/Section'; 4 | import type { Subsection } from '../../models/Subsection'; 5 | import type { SubsectionElement } from '../../models/SubsectionElement'; 6 | import type { TimeInterval } from '../../models/SubsectionTimeInterval'; 7 | const dateRange = 8 | ''; 9 | const locationSVG = 10 | ''; 11 | export function creatreSectionDefinition(section: Section): Content { 12 | return [ 13 | { text: section.name.toUpperCase(), style: 'h2' }, 14 | { 15 | margin: [0, 3, 0, 3], 16 | canvas: [ 17 | { 18 | type: 'line', 19 | x1: 0, 20 | y1: 0, 21 | x2: 514, 22 | y2: 0, 23 | lineWidth: 2, 24 | lineColor: '#000000', 25 | }, 26 | ], 27 | }, 28 | createSubsectionsDefinition(section.subsections), 29 | ]; 30 | } 31 | function createSubsectionsDefinition(subsections: Subsection[]): Content { 32 | const allSubsectionsDoesntHaveChildrens = subsections.every( 33 | (subsection) => subsection.elements.length == 0 34 | ); 35 | if (allSubsectionsDoesntHaveChildrens) { 36 | return { 37 | ul: subsections.map((subsection) => 38 | createSubsectionDefinitionWhitoutElements(subsection) 39 | ), 40 | style: 'ul', 41 | }; 42 | } else { 43 | return subsections.map((subsection) => 44 | createSubsectionDefinition(subsection) 45 | ); 46 | } 47 | } 48 | function createSubsectionDefinitionWhitoutElements( 49 | subsection: Subsection 50 | ): Content { 51 | const result = [{ text: subsection.title, style: 'h4' }] as Content[]; 52 | if (subsection.text) { 53 | result.push({ text: subsection.text, style: 'h5' }); 54 | } 55 | result.push( 56 | createSubsectionTimeIntervalDefinition( 57 | subsection.subsectionTimeInterval 58 | ) 59 | ); 60 | return result; 61 | } 62 | function createSubsectionDefinition(subsection: Subsection): Content { 63 | const result = [{ text: subsection.title, style: 'h3' }] as Content[]; 64 | if (subsection.text) { 65 | result.push({ text: subsection.text, style: 'h4' }); 66 | } 67 | result.push( 68 | createStayDefinition( 69 | subsection.subsectionTimeInterval, 70 | subsection.location 71 | ) 72 | ); 73 | 74 | result.push(createElementsDefinition(subsection.elements)); 75 | return { 76 | stack: result, 77 | style: ['subsection'], 78 | }; 79 | } 80 | 81 | function createStayDefinition( 82 | timeInterval: TimeInterval | undefined, 83 | location: string 84 | ) { 85 | const result = [] as Column[]; 86 | result.push(createSubsectionTimeIntervalDefinition(timeInterval)); 87 | if (!location) { 88 | return { columns: result }; 89 | } 90 | result.push({ 91 | columns: [ 92 | getSVGIconColunm(locationSVG), 93 | { text: location, style: 'h5' }, 94 | ], 95 | style: ['timeInterval'], 96 | }); 97 | return { columns: result }; 98 | } 99 | function createSubsectionTimeIntervalDefinition( 100 | timeInterval: TimeInterval | undefined 101 | ): Content { 102 | let interval = ''; 103 | const result = [] as Column[]; 104 | if (!timeInterval) { 105 | return { columns: [] }; 106 | } 107 | if (timeInterval.dateFrom || timeInterval.dateTo) { 108 | result.push(getSVGIconColunm(dateRange)); 109 | } 110 | if (timeInterval.dateFrom) { 111 | interval += createDateDefinition(timeInterval.dateFrom); 112 | } 113 | interval += ' - '; 114 | if (timeInterval.dateTo) { 115 | interval += createDateDefinition(timeInterval.dateTo); 116 | } else { 117 | interval += 'current'; 118 | } 119 | result.push({ text: interval, style: 'h5' }); 120 | return { columns: result, style: ['timeInterval'] }; 121 | } 122 | function createDateDefinition(date: Date) { 123 | const mount = 124 | date.toLocaleString('en-US', { 125 | month: 'long', 126 | }) ?? ''; 127 | const year = date?.getFullYear().toString() ?? ''; 128 | const simplificedDate = `${mount} ${year}`; 129 | return simplificedDate; 130 | } 131 | function createElementsDefinition(elements: SubsectionElement[]) { 132 | return { 133 | ul: elements.map((element) => ({ text: element.name, style: 'h6' })), 134 | style: 'ul', 135 | }; 136 | } 137 | function getSVGIconColunm(svg: string): Column { 138 | return { 139 | columns: [ 140 | { 141 | svg: svg, 142 | style: ['html-svg', 'h5'], 143 | }, 144 | ], 145 | width: 18, 146 | }; 147 | } 148 | -------------------------------------------------------------------------------- /src/components/app/Editor/TimeInterval/EditorTimeInterval.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 126 | 179 | -------------------------------------------------------------------------------- /src/components/app/Editor/EditorResume.vue: -------------------------------------------------------------------------------- 1 | 86 | 167 | -------------------------------------------------------------------------------- /src/components/app/Editor/Introduction/EditorSocialAccount.vue: -------------------------------------------------------------------------------- 1 | 68 | 69 | 193 | ../../../../stores/ResumeStore 194 | -------------------------------------------------------------------------------- /src/components/app/Editor/List/EditorElement.vue: -------------------------------------------------------------------------------- 1 | 74 | 199 | -------------------------------------------------------------------------------- /src/components/app/Section/CreateSectionModal.vue: -------------------------------------------------------------------------------- 1 | 99 | 238 | 256 | -------------------------------------------------------------------------------- /src/components/app/Editor/Subsection/EditorSubsection.vue: -------------------------------------------------------------------------------- 1 | 80 | 262 | -------------------------------------------------------------------------------- /src/components/app/Editor/Introduction/EditorIntroduction.vue: -------------------------------------------------------------------------------- 1 | 2 | 189 | 190 | 321 | --------------------------------------------------------------------------------