From 21d2ccb4628d4f20f991e0866893d56062ec5e0b Mon Sep 17 00:00:00 2001 From: JesusPerez <jpl@jesusperez.pro> Date: Mon, 10 Jan 2022 11:27:09 +0000 Subject: [PATCH] chore: add code' --- src/App.vue | 21 ++ src/components/Footer.vue | 20 + src/components/MenuLocales.vue | 92 +++++ src/components/MessageBox.vue | 41 +++ src/components/MessageBoxView.vue | 191 ++++++++++ src/components/Modal.vue | 166 +++++++++ src/components/NavMenu.vue | 307 ++++++++++++++++ src/components/Navbar.vue | 76 ++++ src/components/README.md | 9 + src/components/TiptapEditor.vue | 212 +++++++++++ src/composables/dark.ts | 2 + src/composables/index.ts | 1 + src/hooks/config.ts | 110 ++++++ src/hooks/loads.ts | 112 ++++++ src/hooks/tracking.ts | 48 +++ src/hooks/useComponent.ts | 97 +++++ src/hooks/useState.ts | 189 ++++++++++ src/hooks/utils.ts | 178 +++++++++ src/hooks/utilsAuth.ts | 270 ++++++++++++++ src/layouts/Default.vue | 11 + src/layouts/SimpleLayout.vue | 8 + src/logics/dark.ts | 25 ++ src/logics/index.ts | 1 + src/logics/store.ts | 4 + src/main.ts | 35 ++ src/modules/README.md | 11 + src/modules/i18n.ts | 25 ++ src/modules/nprogress.ts | 9 + src/modules/sw.ts | 12 + src/pages/About.md | 21 ++ src/pages/README.md | 20 + src/pages/[...all].vue | 5 + src/pages/base.vue | 50 +++ src/pages/hi/[name].vue | 25 ++ src/router.ts | 56 +++ src/shims.d.ts | 60 +++ src/styles/main.css | 254 +++++++++++++ src/typs/clouds/index.ts | 86 +++++ src/typs/cmpnts/index.ts | 59 +++ src/typs/config.ts | 4 + src/typs/cv.ts | 320 ++++++++++++++++ src/typs/index.ts | 33 ++ src/views/404.vue | 28 ++ src/views/Home.vue | 584 ++++++++++++++++++++++++++++++ src/views/cv/Education.vue | 165 +++++++++ src/views/cv/InfoPanel.vue | 476 ++++++++++++++++++++++++ src/views/cv/Others.vue | 80 ++++ src/views/cv/Profile.vue | 74 ++++ src/views/cv/Projects.vue | 214 +++++++++++ src/views/cv/Skills.vue | 71 ++++ src/views/cv/Talks.vue | 135 +++++++ src/views/cv/Teaching.vue | 135 +++++++ src/views/cv/WorkExperience.vue | 183 ++++++++++ 53 files changed, 5421 insertions(+) create mode 100644 src/App.vue create mode 100644 src/components/Footer.vue create mode 100644 src/components/MenuLocales.vue create mode 100644 src/components/MessageBox.vue create mode 100644 src/components/MessageBoxView.vue create mode 100644 src/components/Modal.vue create mode 100644 src/components/NavMenu.vue create mode 100644 src/components/Navbar.vue create mode 100644 src/components/README.md create mode 100644 src/components/TiptapEditor.vue create mode 100644 src/composables/dark.ts create mode 100644 src/composables/index.ts create mode 100644 src/hooks/config.ts create mode 100644 src/hooks/loads.ts create mode 100644 src/hooks/tracking.ts create mode 100644 src/hooks/useComponent.ts create mode 100644 src/hooks/useState.ts create mode 100644 src/hooks/utils.ts create mode 100644 src/hooks/utilsAuth.ts create mode 100644 src/layouts/Default.vue create mode 100644 src/layouts/SimpleLayout.vue create mode 100644 src/logics/dark.ts create mode 100644 src/logics/index.ts create mode 100644 src/logics/store.ts create mode 100644 src/main.ts create mode 100644 src/modules/README.md create mode 100644 src/modules/i18n.ts create mode 100644 src/modules/nprogress.ts create mode 100644 src/modules/sw.ts create mode 100644 src/pages/About.md create mode 100644 src/pages/README.md create mode 100755 src/pages/[...all].vue create mode 100644 src/pages/base.vue create mode 100644 src/pages/hi/[name].vue create mode 100644 src/router.ts create mode 100644 src/shims.d.ts create mode 100755 src/styles/main.css create mode 100644 src/typs/clouds/index.ts create mode 100644 src/typs/cmpnts/index.ts create mode 100644 src/typs/config.ts create mode 100644 src/typs/cv.ts create mode 100644 src/typs/index.ts create mode 100644 src/views/404.vue create mode 100644 src/views/Home.vue create mode 100644 src/views/cv/Education.vue create mode 100644 src/views/cv/InfoPanel.vue create mode 100644 src/views/cv/Others.vue create mode 100644 src/views/cv/Profile.vue create mode 100644 src/views/cv/Projects.vue create mode 100644 src/views/cv/Skills.vue create mode 100644 src/views/cv/Talks.vue create mode 100644 src/views/cv/Teaching.vue create mode 100644 src/views/cv/WorkExperience.vue diff --git a/src/App.vue b/src/App.vue new file mode 100644 index 0000000..ba38abd --- /dev/null +++ b/src/App.vue @@ -0,0 +1,21 @@ +<template> + <component :is="layout" class="gray-700 dark:gray-200 dark:bg-gray-800 dark:text-warm-gray-100 text-warm-gray-800 dark:bg-warm-gray-800"> + <transition name="slide"> + </transition> + <router-view /> + </component> +</template> +<script setup lang="ts"> +import { useHead } from '@vueuse/head' +useHead({ + title: 'CV', + meta: [ + { name: 'description', content: 'CV Gen' }, + ], +}) +const { currentRoute } = useRouter() +const appLayout = 'AppLayout' +const layout = computed(() => { + return `${currentRoute.value.meta.layout || appLayout}` +}) +</script> \ No newline at end of file diff --git a/src/components/Footer.vue b/src/components/Footer.vue new file mode 100644 index 0000000..0b8e6f4 --- /dev/null +++ b/src/components/Footer.vue @@ -0,0 +1,20 @@ +<script setup lang="ts"> +import { isDark, toggleDark } from '~/composables' +</script> + +<template> + <nav text-xl mt-6 inline-flex gap-2 dark:bg-cool-gray-800 dark:text-white > + <button class="icon-btn !outline-none" @click="toggleDark()"> + <div v-if="isDark" i-carbon-moon /> + <div v-else i-carbon-sun /> + </button> + <a + icon-btn + i-carbon-logo-github + rel="noreferrer" + href="https://github.com/antfu/vitesse-lite" + target="_blank" + title="GitHub" + /> + </nav> +</template> diff --git a/src/components/MenuLocales.vue b/src/components/MenuLocales.vue new file mode 100644 index 0000000..65118b3 --- /dev/null +++ b/src/components/MenuLocales.vue @@ -0,0 +1,92 @@ +<template> + <button class="icon-btn mx-2" @click="toggleShowLocales"> + <carbon-language class="inline-block text-gray-900 dark:text-white" /> + </button> + <div v-if="showLocalesSelector" :class="`select !normal max-w-xs ${selWidth} float-right`"> + <select v-model="currentLocale" class="dark:text-white dark:bg-gray-800 !outline-none"> + <option + v-for="(itm: string) in listLocales" + :key="itm" + :selected="itm === currentLocale" + :value="itm" + > + {{ localeLabel(itm) }} + </option> + </select> + </div> +</template> +<script setup lang="ts"> +// <vue:window on:keydown={cleanOverlay} /> +import { + computed, + PropType, +} from 'vue' + +import { useI18n } from 'vue-i18n' +import {LocalesLabelModes} from '~/typs' + +const props = defineProps({ + labelMode: { + type: String as PropType<LocalesLabelModes>, + //default: LocalesLabelModes.translation, + required: false, + }, + includeCurrent: { + type: Boolean, + default: false, + required: false, + }, +}) +const i18n = useI18n() + +const showLocalesSelector = ref(false) + +const emit = defineEmits(['onLocale']) +const currentLocale = computed({ + get: (): string => i18n.locale.value, + set: (val: string) => { + if (i18n.availableLocales.includes(val)) { + // console.log(`change to ${val} from ${i18n.locale.value}`) + i18n.locale.value = val + emit('onLocale', val) + showLocalesSelector.value = false + } + }, +}) +const locales = computed(() => i18n.availableLocales) +const listLocales = computed(() => + props.includeCurrent ? locales.value : locales.value.filter(lcl => lcl !== currentLocale.value)) +const toggleShowLocales = () => { + showLocalesSelector.value = !showLocalesSelector.value + // change to some real logic + // i18n.locale.value = i18n.availableLocales[(i18n.availableLocales.indexOf(i18n.locale.value) + 1) % i18n.availableLocales.length] +} +const localeLabel = (value: string): string => { + let label = '' + switch (props.labelMode) { + case LocalesLabelModes.auto: + case LocalesLabelModes.translation: + label = i18n.t(value, value) + break + case LocalesLabelModes.value: + label = value + break + } + return label +} +const selWidth = computed(() => { + let width = '' + switch (props.labelMode) { + case LocalesLabelModes.auto: + width = 'w-auto' + break + case LocalesLabelModes.translation: + width = 'w-28' + break + case LocalesLabelModes.value: + width = 'w-24' + break + } + return width +}) +</script> diff --git a/src/components/MessageBox.vue b/src/components/MessageBox.vue new file mode 100644 index 0000000..6109597 --- /dev/null +++ b/src/components/MessageBox.vue @@ -0,0 +1,41 @@ +<template> +<div class="flex"> + <div class="box"> + <div class="text-center space-y-2"> + <div class="space-y-0.5"> + <p class="text-lg text-black dark:text-white font-semibold mb-2"> + <slot name="header"></slot> + </p> + <p class="text-gray-500 dark:text-gray-300 font-medium pb-3"> + <slot name="content"></slot> + </p> + </div> + <slot name="button"></slot> + </div> + </div> +</div> +</template> +<script setup lang="ts"> +/* +import { PropType } from 'vue' +import { OtherType, AuthInfoType, ShowInfoType } from '~/typs/cv' +import useState from '~/hooks/useState' +const props = defineProps({ + data: { + type: Array as PropType<OtherType[]>, + default: [], + required: true, + }, + showinfo: { + type: Object as PropType<ShowInfoType>, + default: {}, + required: true, + }, + authinfo: { + type: Object as PropType<AuthInfoType>, + default: { editable: false, viewchange: false }, + required: true, + }, +}) +*/ +</script> \ No newline at end of file diff --git a/src/components/MessageBoxView.vue b/src/components/MessageBoxView.vue new file mode 100644 index 0000000..22076cb --- /dev/null +++ b/src/components/MessageBoxView.vue @@ -0,0 +1,191 @@ +<template> + <MessageBox v-if="openMessageBox && messageType !== MessageBoxType.NotSet" class="z-99 absolute top-10 left-10 openbox"> + <template v-slot:header> + <h2 class="text-indigo-700 dark:text-indigo-300"> + <span v-if="messageType === MessageBoxType.Select">{{ t('selectModel', 'Select Model') }}</span> + <span v-if="messageType === MessageBoxType.Save"> {{ t('DataNeedSaved', 'Data changes not saved') }}</span> + </h2> + </template> + <template v-slot:content> + <div class="task" v-if="messageType === MessageBoxType.Save && data_url_encoded !== MessageBoxType.NotSet"> + <a + :href="`data:${data_url_encoded}`" + :download="`${inputValue}${inputValue.includes('json') ? '' : '.json'}`" + >{{ t('download_json_data', 'download JSON data') }}</a> + </div> + <div + v-if="messageType === MessageBoxType.Save && data_url_encoded !== MessageBoxType.NotSet" + class="mt-2 text-xs text-800 dark:text-indigo-100" + >{{ t('click_link_to_save', 'Click on link to save') }} {{inputValue}}</div> + <form v-if="messageType === MessageBoxType.OneInput" @submit.prevent="onMessageOkBtn" class="w-full max-w-sm"> + <div class="flex items-center border-b border-indigo-500 py-2"> + <input + class="appearance-none bg-transparent border-none w-full text-gray-700 dark:text-gray-200 mr-3 py-1 px-2 leading-tight focus:outline-none" + :type="inputType" + v-model="oneinputValue" + :placeholder="input_placeholder" + aria-label="Full name" + /> + <div v-if="inpType === inputType" i-carbon-view @click="onInputType"/> + <div v-else i-carbon-view-off @click="onInputType"/> + </div> + </form> + <form v-if="messageType === MessageBoxType.Save && show_input" @submit.prevent="onMessageOkBtn" class="w-full max-w-sm"> + <div class="flex items-center border-b border-indigo-500 py-2"> + <input + class="appearance-none bg-transparent border-none w-full text-gray-700 dark:text-gray-200 mr-3 py-1 px-2 leading-tight focus:outline-none" + type="text" + v-model="inputValue" + :placeholder="input_placeholder" + aria-label="Full name" + /> + </div> + <div class="flex items-center mt-2 py-2"> + <span class="text-gray-700 dark:text-gray-500 text-sm">{{t('saveload.saveOptions','Save options')}}:</span> + <div class="mt-0 flex"> + <div v-if="sendurl !== ''" class="mx-2"> + <label class="inline-flex items-center"> + <input type="radio" class="form-radio" name="radio" v-model="inputSaveMode" :checked="inputSaveMode === 'send'" /> + <span class="ml-2 text-sm">{{t('send','Send')}}</span> + </label> + </div> + <div> + <label class="inline-flex items-center"> + <input type="radio" class="form-radio" name="radio" v-model="inputSaveMode" :checked="inputSaveMode !== 'send'"/> + <span class="ml-2 text-sm">{{t('local','Local')}}</span> + </label> + </div> + </div> + <button v-for="(btn:any,idx:number) in input_btns" :key="idx" + class="flex-shrink-0 bg-indigo-500 hover:bg-indigo-700 border-indigo-500 hover:border-indigo-700 text-sm border-4 text-white py-1 px-2 rounded" + :class="{'dark:bg-red-400 bg-red-700': btn.typ === 'cancel'}" + type="button" + @click="onInputBtn(btn)" + >{{ t(btn.title) }}</button> + </div> + </form> + <div v-if="messageType === 'select' && Object.keys(select_ops).length > 0" class="inline-block relative w-64"> + <select + class="block appearance-none w-full bg-white dark:bg-gray-600 border border-gray-400 hover:border-gray-500 px-4 py-2 pr-8 rounded shadow leading-tight focus:outline-none focus:shadow-outline" + v-model="selectValue" + > + <option value=""></option> + <option v-for="(op: any) in select_ops" :key="op.id" :value="op.id">{{op.title}}</option> + </select> + <div + class="pointer-events-none absolute inset-y-0 right-0 flex items-center px-2 dark:text-indigo-200 text-indigo-700 dark:bg-gray-600 border-r-1 border-t-1 border-b-1 border-l-0 border-gray-400 " + > + <svg class="fill-current h-4 w-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20"> + <path d="M9.293 12.95l.707.707L15.657 8l-1.414-1.414L10 10.828 5.757 6.586 4.343 8z" /> + </svg> + </div> + </div> + </template> + <template v-slot:button> + <div class="flex items-center gap-5"> + <button + v-if="messageType === MessageBoxType.Select || (messageType === MessageBoxType.Save && data_url_encoded === '')" + class="btn-msg flex-grow" + @click="onMessageOkBtn" + > + <span v-if="messageType === MessageBoxType.Select">{{ t('select', 'Select') }}</span> + <span v-if="messageType === MessageBoxType.Save && data_url_encoded === ''" >{{ t('save', 'Save') }}</span> + </button> + <button v-if="messageType === MessageBoxType.OneInput" :disabled="oneinputValue === ''" class="btn-msg flex-grow" @click="onMessageOkBtn">{{ t('save', 'Save') }}</button> + <button v-else class="btn-msg flex-grow" @click="onMessageCloseBtn">{{ t('close', 'Close') }}</button> + </div> + </template> + </MessageBox> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import useState from '~/hooks/useState' +import { MessageBoxType, InputBtnsType, ModelType} from '~/typs/cv' +import MessageBox from '~/components/MessageBox.vue' + +const props = defineProps({ + messageType: { + type: String as PropType<MessageBoxType>, + default: MessageBoxType.NotSet, + required: true, + }, + openMessageBox: { + type: Boolean, + default: false, + required: true, + }, + show_input: { + type: Boolean, + default: false, + required: false, + }, + input_btns: { + type: Array as PropType<InputBtnsType[]>, + default: [], + required: false, + }, + input_placeholder: { + type: String, + default: '', + required: false, + }, + select_ops: { + type: Object as PropType<ModelType>, + default: {}, + required: true, + }, + data_url_encoded: { + type: String, + default: '', + required: false, + }, + inpType: { + type: String, + default: 'text', + required: false, + }, +}) +const emit = defineEmits(['onMessageBox','onInput','onLoadModel']) +const router = useRouter() +const inputType = ref(props.inpType) +const sendurl = router.currentRoute.value.meta.sendurl as string || '/' +const inputValue = useState().current_modelid +const oneinputValue = ref('') +const inputSaveMode = ref(sendurl !== '' ? 'send': 'local') +const selectValue = ref(useState().current_model.value.id) + +const t = useI18n().t +const onInputBtn = (btn: InputBtnsType) => { + emit('onInput', btn ) +} +const onMessageOkBtn = () => { + switch(props.messageType) { + case MessageBoxType.Save: + if (inputSaveMode.value === 'send') { + emit('onMessageBox', { src: 'sendata', val: inputValue.value }) + } else { + emit('onMessageBox', { src: 'savedata', val: inputValue.value }) + } + break + case MessageBoxType.Select: + if (useState().current_model.value.id !== selectValue.value) { + emit('onLoadModel', { id: selectValue.value }) + } + emit('onMessageBox', { src: 'done' }) + break + case MessageBoxType.OneInput: + if (oneinputValue.value !== '') { + emit('onMessageBox', { src: 'oneinput', val: oneinputValue.value }) + } + // emit('onMessageBox', { src: 'done' }) + break + } +} +const onInputType = () => { + inputType.value = inputType.value === props.inpType ? 'text' : props.inpType +} +const onMessageCloseBtn = () => { + emit('onMessageBox', { src: 'done' }) +} +</script> \ No newline at end of file diff --git a/src/components/Modal.vue b/src/components/Modal.vue new file mode 100644 index 0000000..2e9b0c3 --- /dev/null +++ b/src/components/Modal.vue @@ -0,0 +1,166 @@ +<template> + <transition id="modal" name="modal"> + <div class="modal-mask"> + <div + class="modal-wrapper" + > + <div + class="modal-container bg-gray-500 dark:bg-gray-600 shadow-inner rounded border border-gray-500 dark:border-gray-400" + :style="`${cssStyle}`" + > + <div class="relative"> + <div + class="absolute top-0 right-2 h-8 w-8 p-2" + > + <button + class="rounded-md text-gray-300 hover:text-white + focus:outline-none focus:ring-2 focus:ring-white + dark:focus:ring-black dark:bg-cool-gray-600 dark:text-white" + @click="OnCloseButton" + > + <span class="sr-only">Close panel</span> + <!-- Heroicon name: outline/x --> + <svg + class="h-6 w-6 dark:text-white dark:hover:text-gray-400" + xmlns="http://www.w3.org/2000/svg" + fill="none" + viewBox="0 0 24 24" + stroke="currentColor" + aria-hidden="true" + > + <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> + </svg> + </button> + </div> + </div> + <div class="modal-header p-4"> + <slot name="header"> + </slot> + </div> + <div class="modal-body pl-4"> + <slot name="body"> + </slot> + </div> + <div class="modal-footer p-4"> + <slot name="footer"> + <button + class="modal-default-button rounded border p-1 text-gray-400 dark:text-gray-200 hover:text-black focus:outline-none focus:ring-2 focus:ring-black dark:hover:text-white dark:focus:ring-white" + @click="OnCloseButton" + > + OK + </button> + </slot> + </div> + </div> + </div> + </div> + </transition> +</template> +<script setup lang="ts"> +import { onMounted, onUnmounted, ref, computed } from 'vue' +import { useI18n } from 'vue-i18n' +import useState from '~/hooks/useState' +const { t } = useI18n() +const props = defineProps({ + cssStyle: { + type: String, + required: false, + default: () => '', + }, +}) + +const emit = defineEmits(['onCloseModal']) + +const OnCloseButton = () => { + useState().showModal.value = false + emit('onCloseModal') +} + +const OnModalMask = (ev: any) => { +} +const OnKeydownEsc = (event: any) => { + switch (event.key) { + case 'Escape': + if (document.getElementById('modal') && useState().showModal.value) { + event.preventDefault() + event.stopImmediatePropagation() + event.stopPropagation() + useState().showModal.value = false + emit('onCloseModal') + } + break + } +} +onMounted(async() => { + document.addEventListener('keydown', OnKeydownEsc) +}) +onUnmounted(async() => { + document.removeEventListener('keydown', OnKeydownEsc) +}) + +</script> +<style scoped> +.modal-mask { + position: fixed; + z-index: 9998; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.75); + display: table; + transition: opacity 0.3s ease; +} + +.modal-wrapper { + display: table-cell; + vertical-align: middle; +} + +.modal-container { + margin: 0px auto; +/* padding: 20px 30px; */ +/* border-radius: 2px; */ + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.33); + transition: all 0.3s ease; + font-family: Helvetica, Arial, sans-serif; +} + +.modal-header h3 { + margin-top: 0; +/* color: #42b983; */ +} + +.modal-body { + margin: 20px 0; +} + +.modal-default-button { + display: block; + margin-top: 1rem; +} + +/* + * The following styles are auto-applied to elements with + * transition="modal" when their visibility is toggled + * by Vue.js. + * + * You can easily play with the modal transition by editing + * these styles. +*/ + +.modal-enter { + opacity: 0; +} + +.modal-leave-active { + opacity: 0; +} + +.modal-enter .modal-container, +.modal-leave-active .modal-container { + -webkit-transform: scale(1.1); + transform: scale(1.1); +} + +</style> diff --git a/src/components/NavMenu.vue b/src/components/NavMenu.vue new file mode 100644 index 0000000..e3d90d4 --- /dev/null +++ b/src/components/NavMenu.vue @@ -0,0 +1,307 @@ +<template> + <nav + class="noprint flex flex-row gap-2 mx-1 mt-1 border-b-1 border-gray-800 dark:border-gray-500 bg-indigo-100 dark:bg-gray-800 dark:text-white" + :class="{ 'fixed z-50 w-full lg:w-screen-xl -top-2 pt-2 border-1': position === NavPosition.header && fixMenu, 'z-1 opacity-20': openMessageBox }" + > + <button + type="button" + class="flex-none h-6 mt-1 lg:hidden px-2 text-gray-300 hover:text-white focus:outline-none focus:text-white" + :class="{ 'transition transform-180': isOpen }" + @click.prevent="isOpen = !isOpen" + > + <svg class="h-6 w-6 fill-current" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"> + <path + v-show="isOpen" + fill-rule="evenodd" + clip-rule="evenodd" + d="M18.278 16.864a1 1 0 0 1-1.414 1.414l-4.829-4.828-4.828 4.828a1 1 0 0 1-1.414-1.414l4.828-4.829-4.828-4.828a1 1 0 0 1 1.414-1.414l4.829 4.828 4.828-4.828a1 1 0 1 1 1.414 1.414l-4.828 4.829 4.828 4.828z" + /> + <path + v-show="!isOpen" + fill-rule="evenodd" + d="M4 5h16a1 1 0 0 1 0 2H4a1 1 0 1 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2zm0 6h16a1 1 0 0 1 0 2H4a1 1 0 0 1 0-2z" + /> + </svg> + </button> + <div + class="flex-grow mt-2 text-indigo-800 dark:text-indigo-500 inline-flex gap-2 dark:bg-cool-gray-800 dark:text-white" + > + <ul + :class="isOpen ? '-mt-3 flex flex-col gap-5 bg-gray-300 pr-3 pb-5 dark:bg-gray-700 w-full' : 'hidden lg:flex lg:flex-row'" + > + <li + :class="isOpen ? 'inline-block pt-5' : ''" + class="nav-item" + style="margin-top: 0px" + @click="onHome()" + > + <carbon-home /> + </li> + <li v-if="needSave" class="nav-item mt-2 lg:-mt-1" @click.prevent="onNeedSaveBtn"> + <div class="flex rounded-full px-2 py-1 dark:bg-gray-600 bg-gray-300"> + <div i-carbon-save /> + <span class="ml-1 mt-0.5 text-xs">{{ t('save', 'Save') }}</span> + </div> + </li> + <li + v-if="currModelid && select_ops && Object.keys(select_ops).length > 0 && (authinfo.viewchange || authinfo.editable)" + class="nav-item mt-2 lg:-mt-1" + @click.prevent="onSelectBtn" + > + <div class="flex rounded-full px-2 py-1 dark:bg-gray-600 bg-gray-300"> + <div i-carbon-list /> + <span class="ml-1 -mt-0.2 text-xs"> {{currModelid}} </span> + </div> + </li> + <li + v-if="position === NavPosition.header && data_sections && data_sections.length > 0" + v-for="(sec) in data_sections" + :key="sec" + :id="`nav-${sec}`" + class="nav-item" + :class="{'hidden': dataAuth && !showinfo[`${sec}_itms`]}" + @click="onNavMenu(`${prefix}-${sec}`)" + >{{ t(`menu.${sec}`, sec) }}</li> + <li + v-if="position === NavPosition.header && showinfo.skills && !show_infopanel" + id="nav-skills" + class="nav-item" + @click="onNavMenu('cv-skills')" + >{{ t('cv.skills', 'Skills') }}</li> + </ul> + </div> + <div + class="flex-none text-indigo-800 dark:text-indigo-500 inline-flex gap-2 dark:bg-cool-gray-800 dark:text-white" + :class="{ 'p-0': fixMenu }" + > + <div v-if="canChange || canWrite || isAdmin" class="hidden lg:block border-gray-400 dark:border-gray-600 border-l"/> + <div v-if="currModelid" class="hidden flex-grow-0"> + <div class="nav-item hover:mt-1 -mt-2 lg:-mt-1 lg:mt-1 flex rounded-full p-1.2 mt-1 border-1 border-gray-500" + @click.prevent="onSelectBtn"> + <div i-carbon-list /> + <span class="ml-1 text-xs"> {{currModelid}} </span> + </div> + </div> + <div v-if="useEdit || dataAuth && (canWrite || isAdmin)" class="flex-grow-0 icon-btn"> + <div + class="flex icon-btn nav-item hover:mt-1 my-1.5 px-0.8 pb-0.8 rounded-sm" + :class="{ 'bg-indigo-700 text-gray-100 hover:text-gray-300 dark:bg-indigo-300 dark:text-gray-700 dark:hover:text-gray-600': authinfo.editable && needSave }" + @click="onEdit()" + > + <carbon-edit /> + <span v-if="authinfo.editable" class="ml-2 mt-1 text-xs">{{ t('edit', 'Edit') }}</span> + </div> + </div> + <div v-if="dataAuth && (canChange || isAdmin)" class="flex-grow-0 icon-btn"> + <div class="flex icon-btn nav-item hover:mt-1 my-1.5" @click="onViewChange()"> + <carbon-erase /> + <span + v-if="authinfo.viewchange" + class="ml-2 mt-1 text-xs" + >{{ t('changeview', 'Change View') }}</span> + </div> + </div> + <div class="hidden lg:block mx-1 border-gray-400 dark:border-gray-600 border-l"/> + <div v-if="useInfoPanel" class="flex-grow-0 icon-btn ml-3"> + <button class="flex flex-row my-2" @click="onInfoPanel"> + <svg width="1.2em" height="1.2em" class="panel-visibility-toggle"> + <path + v-if="!show_infopanel" + class="eye-open" + d="M8 2.36365C4.36364 2.36365 1.25818 4.62547 0 7.81819C1.25818 11.0109 4.36364 13.2727 8 13.2727C11.6364 13.2727 14.7418 11.0109 16 7.81819C14.7418 4.62547 11.6364 2.36365 8 2.36365ZM8 11.4546C5.99273 11.4546 4.36364 9.82547 4.36364 7.81819C4.36364 5.81092 5.99273 4.18183 8 4.18183C10.0073 4.18183 11.6364 5.81092 11.6364 7.81819C11.6364 9.82547 10.0073 11.4546 8 11.4546ZM8 5.63637C6.79273 5.63637 5.81818 6.61092 5.81818 7.81819C5.81818 9.02547 6.79273 10 8 10C9.20727 10 10.1818 9.02547 10.1818 7.81819C10.1818 6.61092 9.20727 5.63637 8 5.63637Z" + /> + <path + v-else + class="eye-closed" + fill-rule="evenodd" + clip-rule="evenodd" + d="M14.8222 1.85355C15.0175 1.65829 15.0175 1.34171 14.8222 1.14645C14.627 0.951184 14.3104 0.951184 14.1151 1.14645L12.005 3.25653C10.8901 2.482 9.56509 1.92505 8.06 1.92505C3 1.92505 0 7.92505 0 7.92505C0 7.92505 1.16157 10.2482 3.25823 12.0033L1.19366 14.0679C0.998396 14.2632 0.998396 14.5798 1.19366 14.775C1.38892 14.9703 1.7055 14.9703 1.90076 14.775L14.8222 1.85355ZM4.85879 10.4028L6.29159 8.96998C6.10643 8.66645 6 8.3089 6 7.92505C6 6.81505 6.89 5.92505 8 5.92505C8.38385 5.92505 8.7414 6.03148 9.04493 6.21664L10.4777 4.78384C9.79783 4.24654 8.93821 3.92505 8 3.92505C5.8 3.92505 4 5.72505 4 7.92505C4 8.86326 4.32149 9.72288 4.85879 10.4028ZM11.8644 6.88906L13.8567 4.8968C15.2406 6.40616 16 7.92505 16 7.92505C16 7.92505 13 13.925 8.06 13.925C7.09599 13.925 6.20675 13.7073 5.39878 13.3547L6.96401 11.7895C7.29473 11.8779 7.64207 11.925 8 11.925C10.22 11.925 12 10.145 12 7.92505C12 7.56712 11.9529 7.21978 11.8644 6.88906ZM9.33847 9.41501L9.48996 9.26352C9.44222 9.31669 9.39164 9.36726 9.33847 9.41501Z" + /> + </svg> + <span class="nav-item ml-2">Panel</span> + </button> + </div> + <div class="flex-grow-0 icon-btn"> + <div class="icon-btn nav-item hover:mt-1 my-1.5" + :class="{'border-1 p-1 rounded-full mt-0.3 border-gray-900 dark:border-gray-100': fixMenu}" @click="onFixMenu()"> + <div v-if="fixMenu" i-carbon-pin/> + <div v-else i-carbon-pin-filled /> + </div> + </div> + <div class="hidden lg:block mx-1 border-gray-400 dark:border-gray-600 border-l"/> + <div class="flex-grow-0 icon-btn"> + <button class="nav-item hover:mt-1 my-1.5 !outline-none" @click="toggleDark()"> + <div v-if="isDark" i-carbon-sun /> + <div v-else i-carbon-moon /> + </button> + </div> + <div v-if="useLogin" class="nav-item hover:mt-1 my-1.5 !outline-none"> + <router-link v-if="is_logged_user()" to="Logout"> + <div i-carbon-logout /> + </router-link> + <router-link v-else to="Login"> + <div i-carbon-login /> + </router-link> + </div> + <div class="ml-2 flex-grow-0"> + <MenuLocales :label-mode="localesLabelMode" :include-current="includeCurrLocale" @onLocale="onLocale" /> + </div> + </div> + </nav> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { useRouter } from 'vue-router' +import { useI18n } from 'vue-i18n' +import { NavPosition,ShowInfoType,AuthInfoType } from '~/typs/cv' +import { isDark, toggleDark } from '~/composables' +import MenuLocales from '@/MenuLocales.vue' +import {LocalesLabelModes} from '~/typs' +import {is_logged_user} from '~/hooks/utilsAuth' +import useState from '~/hooks/useState' +const props = +defineProps({ + position: { + type: String as PropType<NavPosition>, + default: '', + required: true, + }, + fixMenu: { + type: Boolean, + default: false, + required: true, + }, + show_infopanel: { + type: Boolean, + default: false, + required: true, + }, + dataAuth: { + type: Boolean, + default: true, + required: false, + }, + useInfoPanel: { + type: Boolean, + default: true, + required: false, + }, + useLogin: { + type: Boolean, + default: true, + required: false, + }, + showinfo: { + type: Object as PropType<ShowInfoType>, + default: {}, + required: true, + }, + authinfo: { + type: Object as PropType<AuthInfoType>, + default: { editable: false, viewchange: false, show: true }, + required: true, + }, + openMessageBox: { + type: Boolean, + default: false, + required: true, + }, + needSave: { + type: Boolean, + default: false, + required: true, + }, + useEdit: { + type: Boolean, + default: false, + required: false, + }, + prefix: { + type: String, + default: 'cv', + required: true, + }, +}) +const emit = defineEmits(['onNavMenu']) + +const localesLabelMode = ref(LocalesLabelModes.auto) +const includeCurrLocale = true +const t = useI18n().t +const router = useRouter() +const currModelid = useState().current_modelid +const routeKy = router.currentRoute.value.params.ky || router.currentRoute.value.query.k || '' +const isOpen = ref(false) +const onInfoPanel = () => { + emit('onNavMenu', { src: 'infopanel' }) +} +const onFixMenu = () => { + emit('onNavMenu', { src: 'fixmenu' }) +} +const onLocale = (target: string) => { + emit('onNavMenu',{src: 'locale', target}) +} +const isAdmin = computed(() => { + return routeKy !== '' && useState().showinfo.value.ky === routeKy ? useState().showinfo.value.admin : false +}) +const canWrite = computed(() => { + const val = routeKy !== '' && useState().showinfo.value.ky === routeKy ? useState().showinfo.value.write : false + //auth_info.value.editable = val + return val +}) +const canChange = computed(() => { + const val = routeKy !== '' && useState().showinfo.value.ky === routeKy ? useState().showinfo.value.change : false + //auth_info.value.viewchange = val + return val +}) +const select_ops = useState().selectOps +const data_sections = useState().dataSections +const goToItem = (item: string) => { + const dom_id = document.getElementById(item) + if (dom_id) { + dom_id.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) + setTimeout(() => window.scrollBy(0, isOpen.value ? -140 : -40), 4000) + isOpen.value = false + } +} +const onNavMenu = (item: string) => { + let dom_id = document.getElementById(item) + if (dom_id) { + goToItem(item) + } else { + const itm = `${item.replace(props.prefix, '')}_itms` + if (typeof useState().showinfo.value[itm] !== 'undefined') { + useState().showinfo.value[itm] = useState().showinfo.value[itm] = true + setTimeout(() => goToItem(item), 1000) + } else { + const rtitm = item.replace(`${props.prefix}-`,'') + const rt = router.getRoutes().filter(rt => rt.name === rtitm) + if (rt[0]) { + router.push(rt[0].path) + } + } + } +} +const onHome = () => { + router.push('/home') +} +const goTopPage = () => { + const dom_body = document.getElementsByTagName('body')[0] + if (dom_body) { + dom_body.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) + } +} +const onEdit = () => { + emit('onNavMenu', { src: 'editable' }) +} +const onViewChange = () => { + emit('onNavMenu', { src: 'viewchange' }) +} +const onSelectBtn = () => { + goTopPage() + emit('onNavMenu', { src: 'select' }) +} +const onNeedSaveBtn = () => { + goTopPage() + emit('onNavMenu', { src: 'save' }) +} +</script> \ No newline at end of file diff --git a/src/components/Navbar.vue b/src/components/Navbar.vue new file mode 100644 index 0000000..96d1b71 --- /dev/null +++ b/src/components/Navbar.vue @@ -0,0 +1,76 @@ +<template> +<nav class="bg-white border-gray-200 px-2"> + <div class="container mx-auto flex flex-wrap items-center justify-between"> + <a href="#" class="flex"> + <svg class="h-10 mr-3" width="51" height="70" viewBox="0 0 51 70" fill="none" xmlns="http://www.w3.org/2000/svg"><g clip-path="url(#clip0)"><path d="M1 53H27.9022C40.6587 53 51 42.7025 51 30H24.0978C11.3412 30 1 40.2975 1 53Z" fill="#76A9FA"></path><path d="M-0.876544 32.1644L-0.876544 66.411C11.9849 66.411 22.4111 55.9847 22.4111 43.1233L22.4111 8.87674C10.1196 8.98051 0.518714 19.5571 -0.876544 32.1644Z" fill="#A4CAFE"></path><path d="M50 5H23.0978C10.3413 5 0 15.2975 0 28H26.9022C39.6588 28 50 17.7025 50 5Z" fill="#1C64F2"></path></g><defs><clipPath id="clip0"><rect width="51" height="70" fill="white"></rect></clipPath></defs></svg> + <span class="self-center text-lg font-semibold whitespace-nowrap">FlowBite</span> + </a> + <div class="flex md:order-2"> + <div class="relative mr-3 md:mr-0 hidden md:block"> + <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none"> + <svg class="w-5 h-5 text-gray-500" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M8 4a4 4 0 100 8 4 4 0 000-8zM2 8a6 6 0 1110.89 3.476l4.817 4.817a1 1 0 01-1.414 1.414l-4.816-4.816A6 6 0 012 8z" clip-rule="evenodd"></path></svg> + </div> + <input type="text" id="email-adress-icon" class="bg-gray-50 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full pl-10 p-2" placeholder="Search..."> + </div> + <button @click.prevent="onUserMenu" type="button" class="mr-3 md:mr-0 bg-gray-800 flex text-sm rounded-full focus:ring-4 focus:ring-gray-300" id="user-menu-button" aria-expanded="false" data-dropdown-toggle="dropdown"> + <span class="sr-only">Open user menu</span> + <img class="h-8 w-8 rounded-full" src="/images/people/profile-picture-3.jpg" alt="user photo"> + </button> + <!-- Dropdown menu --> + <div :class="{hidden: hide_user_menu}" class="absolute bg-white text-base z-50 list-none divide-y divide-gray-100 rounded shadow top-10 right-8" id="dropdown"> + <div class="px-4 py-3"> + <span class="block text-sm">Bonnie Green</span> + <span class="block text-sm font-medium text-gray-900 truncate">name@flowbite.com</span> + </div> + <ul class="py-1" aria-labelledby="dropdown"> + <li> + <a href="#" class="text-sm hover:bg-gray-100 text-gray-700 block px-4 py-2">Dashboard</a> + </li> + <li> + <a href="#" class="text-sm hover:bg-gray-100 text-gray-700 block px-4 py-2">Settings</a> + </li> + <li> + <a href="#" class="text-sm hover:bg-gray-100 text-gray-700 block px-4 py-2">Earnings</a> + </li> + <li> + <a href="#" class="text-sm hover:bg-gray-100 text-gray-700 block px-4 py-2">Sign out</a> + </li> + </ul> + </div> + <button data-collapse-toggle="mobile-menu-2" type="button" class="md:hidden text-gray-400 hover:text-gray-900 focus:outline-none focus:ring-2 focus:ring-blue-300 rounded-lg inline-flex items-center justify-center" aria-controls="mobile-menu-2" aria-expanded="false"> + <span class="sr-only">Open main menu</span> + <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M3 5a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 10a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zM3 15a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1z" clip-rule="evenodd"></path></svg> + <svg class="hidden w-6 h-6" fill="currentColor" viewBox="0 0 20 20" xmlns="http://www.w3.org/2000/svg"><path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"></path></svg> + </button> + </div> + <div class="hidden md:flex justify-between items-center w-full md:w-auto md:order-1" id="mobile-menu-2"> + <ul class="flex-col md:flex-row flex md:space-x-8 mt-4 md:mt-0 md:text-sm md:font-medium"> + <li> + <a href="#" class="bg-blue-700 md:bg-transparent text-white block pl-3 pr-4 py-2 md:text-blue-700 md:p-0 rounded" aria-current="page">Home</a> + </li> + <li> + <a href="#" class="text-gray-700 hover:bg-gray-50 border-b border-gray-100 md:hover:bg-transparent md:border-0 block pl-3 pr-4 py-2 md:hover:text-blue-700 md:p-0">About</a> + </li> + <li> + <a href="#" class="text-gray-700 hover:bg-gray-50 border-b border-gray-100 md:hover:bg-transparent md:border-0 block pl-3 pr-4 py-2 md:hover:text-blue-700 md:p-0">Services</a> + </li> + <li> + <a href="#" class="text-gray-700 hover:bg-gray-50 border-b border-gray-100 md:hover:bg-transparent md:border-0 block pl-3 pr-4 py-2 md:hover:text-blue-700 md:p-0">Pricing</a> + </li> + <li> + <a href="#" class="text-gray-700 hover:bg-gray-50 border-b border-gray-100 md:hover:bg-transparent md:border-0 block pl-3 pr-4 py-2 md:hover:text-blue-700 md:p-0">Contact</a> + </li> + </ul> + </div> + </div> +</nav> + +</template> +<script setup lang="ts" > +// import '@themesberg/flowbite' +// import { isDark, toggleDark } from '~/composables' + const hide_user_menu=ref(true) + const onUserMenu = () => hide_user_menu.value = !hide_user_menu.value + +</script> + diff --git a/src/components/README.md b/src/components/README.md new file mode 100644 index 0000000..ccafa35 --- /dev/null +++ b/src/components/README.md @@ -0,0 +1,9 @@ +## Components + +Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components). + +### Icons + +You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/). + +It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details. diff --git a/src/components/TiptapEditor.vue b/src/components/TiptapEditor.vue new file mode 100644 index 0000000..021cdcc --- /dev/null +++ b/src/components/TiptapEditor.vue @@ -0,0 +1,212 @@ +<template> + <div> + <bubble-menu + class="bubble-menu" + :tippy-options="{ duration: 100 }" + :editor="editor" + v-if="editor" + > + <button + @click="editor.chain().focus().toggleBold().run()" + :class="{ 'is-active': editor.isActive('bold') }" + >Bold</button> + <button + @click="editor.chain().focus().toggleItalic().run()" + :class="{ 'is-active': editor.isActive('italic') }" + >Italic</button> + <button + @click="editor.chain().focus().toggleStrike().run()" + :class="{ 'is-active': editor.isActive('strike') }" + >Strike</button> + </bubble-menu> + + <floating-menu + class="floating-menu" + :tippy-options="{ duration: 100 }" + :editor="editor" + v-if="editor" + > + <button + @click="editor.chain().focus().toggleHeading({ level: 1 }).run()" + :class="{ 'is-active': editor.isActive('heading', { level: 1 }) }" + >H1</button> + <button + @click="editor.chain().focus().toggleHeading({ level: 2 }).run()" + :class="{ 'is-active': editor.isActive('heading', { level: 2 }) }" + >H2</button> + <button + @click="editor.chain().focus().toggleBulletList().run()" + :class="{ 'is-active': editor.isActive('bulletList') }" + >Bullet List</button> + </floating-menu> + <editor-content :editor="editor" /> + </div> +</template> +<script setup lang="ts"> +import { + Editor, EditorContent, + FloatingMenu, + BubbleMenu +} from '@tiptap/vue-3' +import { PropType } from 'vue' +import Link from '@tiptap/extension-link' +import StarterKit from '@tiptap/starter-kit' +import TextStyle from '@tiptap/extension-text-style' +import { HtmlAttrsType } from '~/typs/cv' +// import { auth_data } from '~/hooks/utils' +import useState from '~/hooks/useState' +const props = defineProps({ + data: { + type: String, + default: '', + required: true, + }, + src: { + type: String, + default: '', + required: true, + }, + field: { + type: [String, Number], + default: '', + required: true, + }, + idx: { + type: Number, + default: -1, + required: true, + }, + editable: { + type: Boolean, + default: false, + required: true, + }, + htmlattrs: { + type: Object as PropType<HtmlAttrsType>, + default: useState().htmlAttrs, + required: false, + }, +}) +const emit = defineEmits(['onEditorBlur']) +const editor = new Editor({ + extensions: [ + StarterKit.configure({ + // history: false, + bold: { + HTMLAttributes: { + class: props.htmlattrs.bold ? props.htmlattrs.bold : useState().htmlAttrs.bold, + }, + }, + listItem: { + HTMLAttributes: { + class: props.htmlattrs.list ? props.htmlattrs.list : useState().htmlAttrs.list, + }, + }, + }), + TextStyle.configure({ + HTMLAttributes: { + class: props.htmlattrs.text ? props.htmlattrs.text : useState().htmlAttrs.text, + }, + }), + Link.configure({ + openOnClick: false, + HTMLAttributes: { + class: props.htmlattrs.link ? props.htmlattrs.link : useState().htmlAttrs.link, + }, + }), + ], + content: props.data, + editorProps: { + attributes: { + class: 'focus:p-2 focus:dark:bg-gray-700 focus:bg-gray-100 focus:border-gray-500 focus:border-1 prose prose-sm sm:prose lg:prose-lg xl:prose-2xl mx-auto focus:outline-none', + }, + transformPastedText(text) { + return text.toUpperCase() + } + }, + // '<p>Iām running Tiptap with Vue.js. š</p>', + onBeforeCreate({ editor }) { + // Before the view is created. + }, + onCreate({ editor }) { + // The editor is ready. + //editor.commands.setContent(data) + // console.log('debugger create '+editor.state) + }, + onUpdate({ editor }) { + // The content has changed. + }, + onBlur({ editor, event }) { + const data = editor.getHTML() + emit('onEditorBlur', { src: props.src, field: props.field, idx: props.idx, data, ev: event }) + }, + injectCSS: true, + editable: props.editable, +}) +onBeforeMount(async () => { + // const d = useState() + // console.log('Editor: ' + props.data) +}) +onBeforeUnmount(async () => { + editor.destroy() + // console.log('Editor: destroy') +}) +</script> +<style scoped> +/* Basic editor styles */ +/* .ProseMirror { + > * + * { + margin-top: 0.75em; + } + + ul, + ol { + padding: 0 1rem; + } + + blockquote { + padding-left: 1rem; + border-left: 2px solid rgba(#0D0D0D, 0.1); + } +} */ + +.bubble-menu { + display: flex; + background-color: #0D0D0D; + padding: 0.2rem; + border-radius: 0.5rem; +} +.bubble-menu button { + border: none; + background: none; + color: #FFF; + font-size: 0.85rem; + font-weight: 500; + padding: 0 0.2rem; + opacity: 0.6; + } +.bubble-menu button:hover, +.bubble-menu button.is-active { + opacity: 1; +} +.floating-menu { + display: flex; + background-color: #0D0D0D10; + padding: 0.2rem; + border-radius: 0.5rem; +} +.dark .floating-menu { background-color: #0D0D0D;} +.floating-menu button { + border: none; + background: none; + font-size: 0.85rem; + font-weight: 500; + padding: 0 0.2rem; + border-left: #7c7c7c 1px solid; + opacity: 0.6; +} +.floating-menu button:hover, +.floating-menu button.is-active { + opacity: 1; +} +</style> \ No newline at end of file diff --git a/src/composables/dark.ts b/src/composables/dark.ts new file mode 100644 index 0000000..a2a21dd --- /dev/null +++ b/src/composables/dark.ts @@ -0,0 +1,2 @@ +export const isDark = useDark() +export const toggleDark = useToggle(isDark) diff --git a/src/composables/index.ts b/src/composables/index.ts new file mode 100644 index 0000000..e8d1566 --- /dev/null +++ b/src/composables/index.ts @@ -0,0 +1 @@ +export * from './dark' diff --git a/src/hooks/config.ts b/src/hooks/config.ts new file mode 100644 index 0000000..dcaeb71 --- /dev/null +++ b/src/hooks/config.ts @@ -0,0 +1,110 @@ +// import store from '~/store' +import useState from '~/hooks/useState' +import { show_message, fetch_json } from '~/hooks/utils' +import { MessageType} from '~/typs' +import 'toastify-js/src/toastify.css' +import YAML from 'yaml' + +export const set_config = (res: any) => { + if (res && res.URLS && res.URLS.root) { + Object.keys(res.URLS).forEach(it => { + if (it === 'root' || res.URLS[it].includes('yaml') || res.URLS[it].includes('json') ) + useState().CONFURLS.value[it] = res.URLS[it] + else + useState().CONFURLS.value[it] = res.URLS.root.includes(':') ? `${res.URLS.root}${res.URLS[it]}` : res.URLS[it] + }) + } + if (res && res.ASSETS_PATH) + useState().ASSETS_PATH.value = res.ASSETS_PATH + if (res && res.DATA_PATH) + useState().DATA_PATH.value = res.DATA_PATH + if (res && res.MODEL_ID) + useState().MODEL_ID.value = res.MODEL_ID + if (res && res.URLKEY) + useState().URLKEY.value = res.URLKEY + if (res && res.APPNAME) + useState().APPNAME.value = res.APPNAME + if (res && res.AUTHKEY) + useState().AUTHKEY.value = res.AUTHKEY + if (res && res.AUTH_SEPCHAR) + useState().AUTH_SEPCHAR.value = res.AUTH_SEPCHAR + if (res && res.UUIDONE) + useState().UUIDNONE.value = res.UUIDNONE + if (res && res.TKNLIMIT) + useState().TKNLIMIT.value = res.TKNLIMIT + if (res && res.REFRESHTIME) + useState().REFRESHTIME.value = res.REFRESHTIME + if (res && res.ALLOW_REGISTER) + useState().ALLOW_REGISTER.value = res.ALLOW_REGISTER + if (res && res.RESET_PASSWORD) + useState().RESET_PASSWORD.value = res.RESET_PASSWORD + if (res && res.isDevelmode) + useState().isDevelmode.value = res.isDevelmode + if (res && res.htmlAttrs) + useState().htmlAttrs.value = res.htmlAttrs + if (res && res.dataSections) + useState().dataSections.value = res.dataSections + if (res && res.cv_model) + useState().cv_model.value = res.cv_model +} +export const get_urlpath = async (url: string, mode?: string): Promise<[any, string]> => { + if (url.length == 0) + return [{},'No URL defined'] + let text = '' + let res: any = {} + let error = null + try { + const response = await self.fetch(url, { + method: 'GET', + }) + if (url.includes('.json') || mode === 'json') { + res = await response.json() + } else { + text = await response.text() + } + if (response.ok) { + if (url.includes('.yaml') || mode === 'yaml') { + res = YAML.parse(text) + } else if (!url.includes('.json') && mode !== 'json') { + res = text + } + } else { + error = res.message + } + } + catch (err: any) { + error = err.message + } + return [res,error] +} +export const load_config = async (): Promise<[any, string]> => { + const url = window.CONFIG_LOCATION ? window.CONFIG_LOCATION : '/config.yaml' + // const url = window.CONFIG_LOCATION ? window.CONFIG_LOCATION : 'config.json' + const [res,error] = await get_urlpath(url) + if (error) { + show_message(MessageType.Error, `'Config Load' -> ${error}`, 5000) + useState().connection.value.state = 'connection.error' + return [{},error] + } + return [res, error] +} +export const has_config = async(): Promise<boolean> => { + let url = useState().CONFURLS.value.root || '' + if (url.length === 0) { + const [res,err] = await load_config() + if (err && err.length > 0) { + return false + } + set_config(res) + url = useState().CONFURLS.value.root || '' + if (url.length === 0) + return false + } + return true +} +export default { + set_config, + load_config, + get_urlpath, + has_config, +} \ No newline at end of file diff --git a/src/hooks/loads.ts b/src/hooks/loads.ts new file mode 100644 index 0000000..a417279 --- /dev/null +++ b/src/hooks/loads.ts @@ -0,0 +1,112 @@ +import useState from '~/hooks/useState' +import { fetch_json } from '~/hooks/utils' +import { load_config } from '~/hooks/config' + +export const store_info = (key:string,res: any) => { + useState().cvdata.value = {} + useState().datalang.value = {} + let models = [] + if (res.models) { + models = res.models + } + Object.keys(res.data).forEach(ky => { + if (ky === 'main') { + useState().cvdata.value = res.data.main + if (res.data.main.models) { + res.data.main.models.forEach((mdl: any) => models.push(mdl)) + } + if (res.data.main.showinfo) { + const showItms = res.data.main.showinfo.filter((it: any) => it.ky === key) + if (showItms[0]) { + useState().showinfo.value = showItms[0] + } else { + const showPub = res.data.main.showinfo.filter((it: any) => it.ky === '') + if (showPub[0]) + useState().showinfo.value = showPub[0] + } + } + } else { + useState().datalang.value[ky] = {} + Object.keys(res.data[ky]).forEach(sec => { + if (res.data[ky][sec]) { + useState().datalang.value[ky][sec] = res.data[ky][sec] + } + }) + } + }) + useState().models.value = models + models.forEach((mdl: any) => useState().selectOps.value[mdl.id] = mdl) +} +export const load_data_model = async(id: string, rootpath: string, ky: string, with_auth: boolean, cllbck: any) => { + if (id && useState().selectOps.value[id]) { + const url = rootpath.includes('http') ? `${rootpath}/${id}` : useState().selectOps.value[id].path.replace('~',rootpath) + const [res,err] = await fetch_json(url, 2000, with_auth) + if (err.length === 0 && res) { + store_info(ky,res) + useState().current_model.value = useState().selectOps.value[id] + useState().current_modelid.value = id + if (cllbck) { + cllbck() + } + } + } +} +export const load_data_info = async (to: any, _from: any, next: any) => { + let url = useState().CONFURLS.value.root || '' + if (url.length > 0) { + const [_res, err]= await load_config() + if (err && err.length > 0) { + next() + return + } + } + url = useState().CONFURLS.value.data || '' + if (url.length == 0) { + next() + return + } + url = `${url}/${useState().MODEL_ID.value}` + const [res,errmsg] = await fetch_json(url, 2000,to.meta.withauth) + // fetch_json(url, 2000,to.meta.withauth,(res,errmsg) => { + if (errmsg.length > 0) { + next() + return + } + if (res && res.data) { + const ky = to.params.ky || to.query.k || '' + store_info(ky,res) + if (useState().selectOps.value[useState().MODEL_ID.value]) { + useState().current_model.value = useState().selectOps.value[useState().MODEL_ID.value] + useState().current_modelid.value = useState().MODEL_ID.value + } + } else { + //next('404') + next() + return + } + next() +} +export const load_currentRoute= async(rt: any): Promise<boolean> => { + let url = useState().CONFURLS.value.data || '' + if (url.length == 0) { + return false + } + url = `${url}/${useState().MODEL_ID.value}` + const [res,errmsg] = await fetch_json(url, 2000,rt.meta.withauth) + if (errmsg.length === 0 && res && res) { + const ky = rt.params.ky || rt.query.k || '' + store_info(ky,res) + if (useState().models.value[rt.meta.model]) { + useState().current_model.value = useState().models.value[rt.meta.model] + } + useState().current_modelid.value = rt.meta.model + return true + } + return false +} +export default { + store_info, + load_data_model, + load_data_info, + load_currentRoute, +} diff --git a/src/hooks/tracking.ts b/src/hooks/tracking.ts new file mode 100644 index 0000000..f31d113 --- /dev/null +++ b/src/hooks/tracking.ts @@ -0,0 +1,48 @@ +import useState from '~/hooks/useState' +import { MessageType, TrackActionType } from '~/typs' +import { show_message,send_data} from '~/hooks/utils' + +export const track_action = async(e: any, src?: {ref: string,text?: string}, act = 'click') => { + const url = useState().CONFURLS.value.tracking || '' + if (url.length == 0) { + return + } + let href='' + let text='' + if (src && src.ref && src.ref.length > 0 ) { + href=src.ref + text=src.text ? src.text : src.ref + } else if (e && e.target) { + switch(e.target.tagName) { + case 'IMG': + const imgTarget = e.target.parentNode.tagName === 'A' ? e.target.parentNode : null + if (imgTarget && imgTarget.tagName === 'A') { + href = imgTarget.href ? imgTarget.href : '' + text = e.target.alt ? e.target.alt : e.target.src.slice(-1)[0] + } + break + case 'A': + const linkTarget = e.target + if (linkTarget && linkTarget.tagName === 'A') { + href = linkTarget.href ? linkTarget.href : '' + text = linkTarget.innerHTML ? linkTarget.innerHTML : '' + } + break + } + } + const trackAction: TrackActionType = { + when: Date.now().toString(), + where: `${useState().APPNAME.value}>${text}: ${href}`, + what: act, + context: navigator.userAgent, + data: useState().userID.value, + } + const [_res,err] = await send_data(url, trackAction, true, true) + if (err && err.length > 0 ) { + show_message(MessageType.Error, `Error: ${err}`,2000) + } +} + +export default { + track_action, +} diff --git a/src/hooks/useComponent.ts b/src/hooks/useComponent.ts new file mode 100644 index 0000000..c7dc022 --- /dev/null +++ b/src/hooks/useComponent.ts @@ -0,0 +1,97 @@ +import { ref } from 'vue' + +// import WysiwygEditor from '@/WysiwygEditor.vue' +// // import CodeEditor from '@/CodeEditor.vue' +// import GridSettings from '~/views/GridSettings.vue' +// import GridView from '~/views/GridView.vue' +// import TableView from '~/views/TableView.vue' +// import ListView from '~/views/ListView.vue' +// import FormView from '~/views/FormView.vue' + +// import TaFormView from '/app_modules/bm/ta/views/ta_form.vue' +// import TaTableView from '/app_modules/bm/ta/views/ta_table.vue' +// import TaListView from '/app_modules/bm/ta/views/ta_list.vue' + +export enum DynComponent { + // GridSettings, + // GridJs, + // TableView, + // ListView, + // FormView, + // WysiwygEditor, +// CodeEditor, +} + +const asideComponent = ref({}) + +const settingsComponent = ref({}) + +const fullSliderComponent = ref({}) + +const formViewComponent = ref({}) + +const dataViewComponent = ref({}) + +const topPaneComponent = ref({}) + +const bottomPaneComponent = ref({}) + +const moduleComponent = ref({}) + +const getModuleComponent = (key: string, target: string): any => { + // switch (key) { + // case 'ta': + // switch (target) { + // case 'form': + // return TaFormView + // break + // case 'table': + // return TaTableView + // break + // case 'list': + // return TaListView + // break + // } + // break + // } +} +const getComponent = (cmpnt: DynComponent): any => { + // switch (cmpnt) { + // case DynComponent.WysiwygEditor: + // return WysiwygEditor + // break + // // case DynComponent.CodeEditor: + // // return CodeEditor + // // break + // case DynComponent.GridSettings: + // return GridSettings + // break + // case DynComponent.GridJs: + // return GridView + // break + // case DynComponent.TableView: + // return TableView + // break + // case DynComponent.ListView: + // return ListView + // break + // case DynComponent.FormView: + // return FormView + // break + // } +} + +export default function useComponent() { + return { + getModuleComponent, + asideComponent, + settingsComponent, + fullSliderComponent, + dataViewComponent, + formViewComponent, + getComponent, + topPaneComponent, + bottomPaneComponent, + moduleComponent, + } +} diff --git a/src/hooks/useState.ts b/src/hooks/useState.ts new file mode 100644 index 0000000..3607cd6 --- /dev/null +++ b/src/hooks/useState.ts @@ -0,0 +1,189 @@ +import { ref } from 'vue' +import type { SideMenuItemType, UiPanelsType } from '~/typs/cmpnts' +import { DataLangsType, ShowInfoType, HtmlAttrsType, ModelType, ModelSelType, CVDataType } from '~/typs/cv' +import { ConfUrlsType } from '~/typs/config' + +const reqError = ref({ defs: '', lang: '', data: '', gql: '', api: '' }) + +const currentMapKey = ref('') +const isSidebarOpen = ref(false) +const toggleSidebar = () => { + isSidebarOpen.value = !isSidebarOpen.value +} +const showModal = ref(false) + +const isAsidePanelOpen = ref(false) + +const sideSettingButton = ref(false) + +const isSettingsPanelOpen = ref(false) + +const isSearchPanelOpen = ref(false) +const usideSettingButton = ref(false) + +const isNotificationsPanelOpen = ref(false) + +const useSettings = ref(false) + +const useSearch = ref(false) + +const search = ref('') + +const isDevelmode = ref(false) + +const bcPath = ref('') + +const navTitle = ref({ + text: '', + textclick: null as null | Function, + title: '', + cmpnt: '', + ops: [] as any[], + btntype: '', + cllbck: null as null | Function, +}) +const dfltNavTitle = () => { + navTitle.value = { + text: '', + textclick: null as null | Function, + title: '', + cmpnt: '', + ops: [] as any[], + btntype: '', + cllbck: null as null | Function, + } +} + +const bookCllbck = ref() +const bookSelec = (data: string) => { + if (data === '#') { + bcPath.value = '' + dfltNavTitle() + } + else { + if (bookCllbck.value) + bookCllbck.value(data) + // else ... + } +} +const sidebarMenuItems = ref([] as SideMenuItemType[]) + +const checkin = ref(false) +const connection = ref({ + state: '', +}) + +const side_menu_click = ref() +const app_home_click = ref() + +const backdrop_blur = ref(14) +const back_opacity = ref(60) +const panels = ref({} as UiPanelsType) + +const show_profile = ref(false) + +const cvdata = ref({} as CVDataType | any) +const datalang = ref({} as DataLangsType | any) +const showinfo = ref({} as ShowInfoType) +const authinfo = ref({ editable: false, viewchange: false, show: true }) + +const dataSections = ref([] as string[]) +const htmlAttrs = ref({ + bold: '', //'itm-title', + list: 'list-circle ml-5', + link: 'link', + text: '', +} as HtmlAttrsType) + +const cv_model = ref({} as ModelType) +const models = ref([] as ModelType[]) +const selectOps = ref({} as ModelSelType) +const current_model = ref({} as ModelType) +const current_modelid = ref("") + +const APPNAME = ref('') +const AUTHKEY = ref('auth') +const UUIDNONE = ref('none') +const TKNLIMIT = ref(20) +const REFRESHTIME = ref(5) +const ASSETS_PATH = ref("/assets") +const DATA_PATH = ref("/assets/data") +// const DATA_URL = ref("${DATA_PATH}/dist/info.json") +const MODEL_ID = ref("cv") +const URLKEY = ref("?k: ") // location.search.replace('?k: ','') +const AUTH_SEPCHAR = ref(";") +const PASSWD_ENC = ref("enc;") +const ALLOW_REGISTER = ref(false) +const RESET_PASSWORD = ref(false) + +const CONFURLS = ref({} as ConfUrlsType) + +const timeoutAuth = ref(0) +const allowView = ref(false) +const userID = ref('') + +const sourceRoute = ref('') + +export default function useState() { + return { + reqError, + currentMapKey, + isSidebarOpen, + toggleSidebar, + isAsidePanelOpen, + isSettingsPanelOpen, + isSearchPanelOpen, + isNotificationsPanelOpen, + useSettings, + usideSettingButton, + sideSettingButton, + bcPath, + bookSelec, + bookCllbck, + navTitle, + dfltNavTitle, + checkin, + connection, + sidebarMenuItems, + showModal, + side_menu_click, + app_home_click, + useSearch, + search, + back_opacity, + backdrop_blur, + panels, + show_profile, + cvdata, + datalang, + showinfo, + authinfo, + dataSections, + htmlAttrs, + selectOps, + models, + cv_model, + current_model, + current_modelid, + timeoutAuth, + isDevelmode, + allowView, + userID, + sourceRoute, + // APP CONFIG VARS + APPNAME, + AUTHKEY, + UUIDNONE, + TKNLIMIT, + REFRESHTIME, + ASSETS_PATH, + DATA_PATH, + MODEL_ID, + URLKEY, + AUTH_SEPCHAR, + PASSWD_ENC, + ALLOW_REGISTER, + RESET_PASSWORD, + CONFURLS, + } +} diff --git a/src/hooks/utils.ts b/src/hooks/utils.ts new file mode 100644 index 0000000..dad864b --- /dev/null +++ b/src/hooks/utils.ts @@ -0,0 +1,178 @@ +import Toastify from 'toastify-js' +import useState from '~/hooks/useState' +import { checkAuth} from '~/hooks/utilsAuth' +import { MessageType} from '~/typs' +import 'toastify-js/src/toastify.css' + +export const show_message = (typ: MessageType, msg: string, timeout?: number, cllbck?: any): void => { + let cssClass = 'msg-box' + switch (typ) { + case MessageType.Show: + cssClass += ' msg-show' + break + case MessageType.Success: + cssClass += ' msg-ok' + break + case MessageType.Error: + cssClass += ' msg-error' + break + case MessageType.Warning: + cssClass += ' msg-warn' + break + case MessageType.Info: + cssClass += ' msg-info' + break + } + // https://github.com/apvarun/toastify-js + Toastify({ + text: msg, + duration: timeout || 3000, + //destination: '', + //selector: '', + // newWindow: true, + // close: true, + className: cssClass, + offset: { + x: 10, // horizontal axis - can be a number or a string indicating unity. eg: '2em' + y: 50, // vertical axis - can be a number or a string indicating unity. eg: '2em + }, + gravity: 'top', // `top` or `bottom` + position: 'left', // `left`, `center` or `right` + // backgroundColor: 'linear-gradient(to right, #00b09b, #96c93d)', + stopOnFocus: true, // Prevents dismissing of toast on hover + callback: cllbck + // onClick() {}, // Callback after click + }).showToast() +} + +export const translate = (store: any, map: string, ky: string, ctx: string, dflt?: string): string => { + const val = dflt || ky + const lang: any = {} + if (lang && lang.value) + return lang.value[ctx] && lang.value[ctx][ky] ? lang.value[ctx][ky] : val + else if (lang && lang[ctx]) + return lang[ctx][ky] ? lang[ctx][ky] : val + else + return val +} +export const fetch_text = async(path: RequestInfo): Promise<any> => { + try { + const response = await fetch(path) + return !response.ok ? response.text() : new Error('No items found') + } + catch (err) { + return err + } +} +const request_headers = async (with_auth: boolean): Promise<[any, string]> => { + const headers: any = {} // { 'Content-Type': 'application/json' } + headers['Accept']='application/json' + let error= '' + if (with_auth) { + const token = await checkAuth() + if (token && token.length > 0) { + headers['Authorization'] =`Bearer ${token}` + } else { + return [null,`error no auth found`] + } + } + return [headers, error] +} +export const fetch_json = async(path: RequestInfo, timeout = 2000, with_auth = false, cllbck?: any): Promise<any> => { + useState().connection.value.state = '' + let res = null + let error = '' + const showError = (err: string) => { + show_message(MessageType.Error, `'Data Load' -> ${err}`,5000) + useState().connection.value.state = 'connection.error' + } + const [headers,errorHeader] = await request_headers(with_auth) + if (errorHeader.length > 0) { + return [null,errorHeader] + } + try { + const response = await self.fetch(path, { + method: 'GET', + headers, + //mode: 'cors', + }) + res = await response.json() + if (!response.ok && res.message.length > 0) + error=res.message + } + catch (err: any) { + error = err + } + if (error.length > 0) + showError(error) + return [res,error] +} +export const send_data = async(url: string, formData: any, with_auth = true, show = true, cllbck?: any): Promise<any> => { + const [headers,errorHeader] = await request_headers(with_auth) + if (errorHeader.length > 0) { + return [null,errorHeader] + } + headers['Content-Type'] = 'application/json' + let formDataJsonString = '' + try { + formDataJsonString = JSON.stringify(formData) + } + catch (e) { + console.log(e) + return [null,e] + } + try { + const response = await fetch(url, { + method: 'POST', + headers, + body: formDataJsonString, + }) + if (!response.ok && response.status !== 200) { + const errorMessage = await response.json() + console.log(errorMessage) + if (show) + show_message(MessageType.Error, `Send data -> ${errorMessage.error ? errorMessage.error : ''}`) + return [null,errorMessage] + // throw new Error(errorMessage) + } else if (response.ok && response.status === 200) { + const res = await response.json() + if (cllbck) { + cllbck(res) + } + return [res,null] + } + } + catch (e) { + if (show) + show_message(MessageType.Error, `Send data -> ${e}`) + useState().connection.value.state = 'connection.error' + console.log(e) + return [null,e] + } +} +export const run_task = (val: number, task: Function) => { + const now = new Date().getTime() + const timePassed = now % val + const run_at = val - timePassed + setTimeout(task, run_at) +} + +export const parse_str_json_data = (src: string, dflt: object|any):object|any => { + let data = dflt + try { + data = JSON.parse(src) + } + catch (e) { + data = dflt + } + return data +} +export default { + fetch_text, + fetch_json, + send_data, + show_message, + translate, + run_task, + parse_str_json_data, +} diff --git a/src/hooks/utilsAuth.ts b/src/hooks/utilsAuth.ts new file mode 100644 index 0000000..0616650 --- /dev/null +++ b/src/hooks/utilsAuth.ts @@ -0,0 +1,270 @@ +import useState from '~/hooks/useState' +import { MessageType} from '~/typs' +import { show_message } from './utils.js' + +export const check_credentials = async(url: string, data: any): Promise<any> => { + let dataJsonString = '' + try { + dataJsonString = JSON.stringify(data) + } + catch (e) { + console.log(e) + return + } + try { + const response = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: dataJsonString, + }) + if (!response.ok) { + const errorMessage = await response.text() + // throw new Error(errorMessage) + console.log(errorMessage) + return + } + if (response.ok && response.status === 200) + return response.json() + } + catch (e) { + useState().connection.value.state = 'connection.error' + console.log(e) + } +} +export const parseJwt = (token: string|null): any => { + let res = {} + if (token && token.length === 0 || token === "?") { + return res + } + if (token) { + try { + const base64Url = token.split('.')[1]; + const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/'); + const jsonPayload = decodeURIComponent(atob(base64).split('').map((c) => + '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2) + ).join('')); + res = JSON.parse(jsonPayload); + } catch(e) { + console.log(e) + localStorage.removeItem(useState().AUTHKEY.value) + } + } + return res +} +export const authExpired = (): boolean => { + const payload = parseJwt(localStorage.getItem(useState().AUTHKEY.value)) + if (payload.exp) { + const now = Date.now() / 1000; + return payload.exp - now < 0 + } else { + return true + } +} +export const onTimeoutAuth = () => { + if (useState().timeoutAuth.value > 0) { + if (useState().isDevelmode.value) + console.log(`timeout already set to: ${useState().timeoutAuth.value}`) + return + } + const payload = parseJwt(localStorage.getItem(useState().AUTHKEY.value)) + const now = Date.now() / 1000; + if (payload.exp) { + let timeout = Math.round(payload.exp - now) + const limit = useState().TKNLIMIT.value * 60 + timeout = timeout < limit ? timeout - (useState().REFRESHTIME.value * 60) : limit - (useState().REFRESHTIME.value * 60) + timeout = timeout < 0 ? 100 : timeout + useState().timeoutAuth.value = timeout + if (useState().isDevelmode.value) + console.log(`AUTH timeout(${limit}): ${timeout} [${Math.round(payload.exp - now)}]`) + setTimeout(() => { + const r = async() => { + useState().timeoutAuth.value=0 + await refreshAuth() + } + r() + }, (timeout*1000)) + } +} +export const refreshAuth = async(val = 0) => { + const url = useState().CONFURLS.value.refreshauth || '' + if (url.length == 0) { + console.log('URL not found for refreshToken') + return + } + let res: any = '', error: any = '' + const headers: any = {} // { 'Content-Type': 'application/json' } + headers['Accept']='application/json' + const token = localStorage.getItem(useState().AUTHKEY.value) + if (token && token.length > 0) { + headers['Authorization'] =`Bearer ${token}` + } + try { + const response = await self.fetch(url, { + method: 'GET', + headers, + //mode: 'cors', + }) + res = await response.json() + if (!response.ok && res.message && res.message.length > 0) + error=res.message + } + catch (err: any) { + error = err.message + } + if (res && res.error && res.error.length > 0) { + error = res.error + } + if (error && error.length > 0) { + show_message(MessageType.Error, `'Auth Refresh' -> ${error}`,5000) + useState().connection.value.state = 'connection.error' + setTimeout(() => { + location.href = '/logout' + }, 1000) + return + } + if (res && res[useState().AUTHKEY.value]) { + localStorage.setItem(useState().AUTHKEY.value,res[useState().AUTHKEY.value]) + console.log(`New auth: ${localStorage.getItem(useState().AUTHKEY.value)}`) + onTimeoutAuth() + } +} +export const getAuth = async(urlpath = '',val = 0): Promise<[any,string]> => { + const urlkey = location.search.replace(useState().URLKEY.value,'') + const uuid = urlkey.length > 0 ? urlkey : useState().UUIDNONE.value + const url = urlpath.length > 0 ? urlpath : (useState().CONFURLS.value.auth ? useState().CONFURLS.value.auth.replace('URLKEY',uuid) : '') + if (url.length === 0) { + console.log('URL not found to get Auth') + return [null,''] + } + let res: any = '' + let errmsg: any = '' + try { + const response = await self.fetch(url, { + method: 'GET', + //mode: 'cors', + }) + res = await response.json() + if (!response.ok && res.message.length > 0) + errmsg=res.message + } + catch (e: any) { + errmsg = e.message + } + if (errmsg && errmsg.length > 0) { + show_message(MessageType.Error, `'Auth Data Load' -> ${errmsg}`,5000) + useState().connection.value.state = 'connection.error' + } + if (res && res.auth) { + if (res.auth === "?") { + useState().userID.value = "?" + } else { + useState().userID.value = res.user ? res.user : '' + localStorage.setItem(useState().AUTHKEY.value,res.auth) + onTimeoutAuth() + } + } + return [res,errmsg] +} +export const checkUserAuth = async(val: string): Promise<boolean> => { + if (val.length === 0) { + return false + } + const urlkey = location.search.replace(useState().URLKEY.value,'') + const uuid = urlkey.length > 0 ? urlkey : useState().UUIDNONE.value + const urlpath = useState().CONFURLS.value.auth ? useState().CONFURLS.value.auth.replace('URLKEY',uuid) : '' + if (urlpath.length === 0) { + console.log('URL not found to check Auth') + return false + } + const psw = btoa(val) + const url = `${urlpath}${useState().AUTH_SEPCHAR.value}${psw}` + const [res,errmsg] = await getAuth(url,0) + if (errmsg.length === 0 && res && res.user) { + useState().allowView.value=true + const payload = parseJwt(localStorage.getItem(useState().AUTHKEY.value)) + if (payload && payload.id && payload.id === res.user) { + return true + } + } + return false +} +export const checkPerms = async(auth: string|null): Promise<boolean> => { + if (auth && auth.length === 0 || auth === '?') + return false + if (useState().allowView.value) + return true + const payload = parseJwt(auth) + if (payload && payload.uuid && payload.uuid !== useState().UUIDNONE.value) { + useState().allowView.value=false + return true + } + useState().allowView.value=true + return true +} +export const checkAuth = async(urlpath = '',val = 0): Promise<string|null> => { + if (authExpired()) { + const [res,errmsg] = await getAuth(urlpath,val) + if (errmsg.length === 0 && res && res.auth) { + return await checkPerms(res.auth) ? res.auth : '' + } + return '' + } + if (location.pathname === '/' && useState().URLKEY.value.length > 0) { + const urlkey = location.search.replace(useState().URLKEY.value,'') + const uuid = urlkey.length > 0 ? urlkey : useState().UUIDNONE.value + const data = authData() + if (data.payload && data.payload.uuid && data.payload.uuid !== uuid) { + const url = useState().CONFURLS.value.auth ? useState().CONFURLS.value.auth.replace('URLKEY',uuid) : '' + if (url.length === 0) { + console.log('URL not found to check Perms') + return '' + } + const [res,_err] = await getAuth(url,val) + if (res.auth) { + return await checkPerms(res.auth) ? res.auth : '' + } + return '' + } else { + onTimeoutAuth() + return await checkPerms(data.auth) ? data.auth : '' + } + } else { + const data = authData() + if (data.payload && data.payload.id) + useState().userID.value = data.payload.id + else + return '' + } + onTimeoutAuth() + const res: string|null = localStorage.getItem(useState().AUTHKEY.value) + return await checkPerms(res) ? res : '' +} +export const authData = ():any => { + const auth = localStorage.getItem(useState().AUTHKEY.value) || '' + const uidefs = {} + const payload = parseJwt(localStorage.getItem(useState().AUTHKEY.value)) + return {auth, payload, uidefs} +} +export const is_logged_user = (): boolean => { + const urlkey = location.search.replace(useState().URLKEY.value, '') + if (urlkey.length > 0) { + if (authExpired()) { + return false + } + return true + } + return false +} +export default { + check_credentials, + parseJwt, + authExpired, + checkAuth, + checkUserAuth, + refreshAuth, + getAuth, + authData, + is_logged_user, +} diff --git a/src/layouts/Default.vue b/src/layouts/Default.vue new file mode 100644 index 0000000..0663a3c --- /dev/null +++ b/src/layouts/Default.vue @@ -0,0 +1,11 @@ +<template> + <main class="px-4 py-10 text-center text-gray-700 dark:text-gray-200"> + <transition name="fade"> + <router-view /> + </transition> + <Footer /> + <div class="mt-5 mx-auto text-center opacity-25 text-sm"> + [Default Layout] + </div> + </main> +</template> diff --git a/src/layouts/SimpleLayout.vue b/src/layouts/SimpleLayout.vue new file mode 100644 index 0000000..0406436 --- /dev/null +++ b/src/layouts/SimpleLayout.vue @@ -0,0 +1,8 @@ +<template> + <div class="antialiased text-gray-900 bg-white"> + <slot /> + </div> + <Footer /> +</template> + +<script setup></script> diff --git a/src/logics/dark.ts b/src/logics/dark.ts new file mode 100644 index 0000000..0bc9bd1 --- /dev/null +++ b/src/logics/dark.ts @@ -0,0 +1,25 @@ +import { watch, computed } from 'vue' +import { usePreferredDark, useToggle } from '@vueuse/core' +import { colorSchema } from './store' + +const preferredDark = usePreferredDark() + +export const isDark = computed({ + get() { + return colorSchema.value === 'auto' ? preferredDark.value : colorSchema.value === 'dark' + }, + set(v: boolean) { + if (v === preferredDark.value) + colorSchema.value = 'auto' + else + colorSchema.value = v ? 'dark' : 'light' + }, +}) + +export const toggleDark = useToggle(isDark) + +watch( + isDark, + v => typeof document !== 'undefined' && document.documentElement.classList.toggle('dark', v), + { immediate: true }, +) diff --git a/src/logics/index.ts b/src/logics/index.ts new file mode 100644 index 0000000..e8d1566 --- /dev/null +++ b/src/logics/index.ts @@ -0,0 +1 @@ +export * from './dark' diff --git a/src/logics/store.ts b/src/logics/store.ts new file mode 100644 index 0000000..7db12c2 --- /dev/null +++ b/src/logics/store.ts @@ -0,0 +1,4 @@ +import { Ref } from 'vue' +import { useStorage } from '@vueuse/core' + +export const colorSchema = useStorage('vueuse-color-scheme', 'auto') as Ref<'auto' | 'dark' | 'light'> diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..826ab44 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,35 @@ +import { createApp } from 'vue' +import App from './App.vue' + +import '@unocss/reset/tailwind.css' +import './styles/main.css' +import 'uno.css' +import { createI18n } from 'vue-i18n' +import { createHead, useHead } from '@vueuse/head' +import { messages } from './modules/i18n' +import VueHighlightJS from 'vue3-highlightjs' +import 'highlight.js/styles/solarized-light.css' + +const app = createApp(App) + +app.use(VueHighlightJS) +import routes from './router' +//import AppLayout from '~/layouts/AppLayout.vue' +import SimpleLayout from '~/layouts/SimpleLayout.vue' + +const i18n = createI18n({ + locale: navigator.language === 'es' ? 'es' : 'en', + legacy: false, + fallbackLocale: ['en'], + fallbackWarn: false, + missing: (locale, key, instance) => { + console.warn(`detect '${key}' key missing in '${locale}'`) + }, + messages, +}) +const head = createHead() +app.use(routes) +app.use(i18n) +app.use(head) +app.component('SimpleLayout', SimpleLayout) +app.mount('#app') diff --git a/src/modules/README.md b/src/modules/README.md new file mode 100644 index 0000000..adda9ef --- /dev/null +++ b/src/modules/README.md @@ -0,0 +1,11 @@ +## Modules + +A custom user module system. Place a `.ts` file with the following template, it will be installed automatically. + +```ts +import { UserModule } from '~/types' + +export const install: UserModule = ({ app, router, isClient }) => { + // do something +} +``` diff --git a/src/modules/i18n.ts b/src/modules/i18n.ts new file mode 100644 index 0000000..f7528a1 --- /dev/null +++ b/src/modules/i18n.ts @@ -0,0 +1,25 @@ +import { createI18n } from 'vue-i18n' +// import { UserModule } from '~/types' + +// import i18n resources +// https://vitejs.dev/guide/features.html#glob-import +export const messages = Object.fromEntries( + Object.entries( + import.meta.globEager('../../locales/*.y(a)?ml')) + .map(([key, value]) => { + const yaml = key.endsWith('.yaml') + return [key.slice(14, yaml ? -5 : -4), value.default] + }), +) +/* +export const install: UserModule = ({ app }) => { + const i18n = createI18n({ + legacy: false, + locale: 'en', + messages, + }) + + app.use(i18n) +} + +*/ \ No newline at end of file diff --git a/src/modules/nprogress.ts b/src/modules/nprogress.ts new file mode 100644 index 0000000..8019b41 --- /dev/null +++ b/src/modules/nprogress.ts @@ -0,0 +1,9 @@ +import NProgress from 'nprogress' +import { UserModule } from '~/types' + +export const install: UserModule = ({ isClient, router }) => { + if (isClient) { + router.beforeEach(() => { NProgress.start() }) + router.afterEach(() => { NProgress.done() }) + } +} diff --git a/src/modules/sw.ts b/src/modules/sw.ts new file mode 100644 index 0000000..fd7f7ca --- /dev/null +++ b/src/modules/sw.ts @@ -0,0 +1,12 @@ +import { UserModule } from '~/types' + +export const install: UserModule = ({ isClient, router }) => { + if (isClient) { + router.isReady().then(async() => { + if (isClient) { + const { registerSW } = await import('virtual:pwa-register') + registerSW({ immediate: true }) + } + }) + } +} diff --git a/src/pages/About.md b/src/pages/About.md new file mode 100644 index 0000000..cb313be --- /dev/null +++ b/src/pages/About.md @@ -0,0 +1,21 @@ +--- +title: About +--- + +<div class="text-center"> + <!-- You can use Vue components inside markdown --> + <carbon-dicom-overlay class="text-4xl mb-6 m-auto" /> + <h3>About</h3> +</div> + +[Vitesse](https://github.com/antfu/vitesse) is an opinionated [Vite](https://github.com/vitejs/vite) starter template made by [@antfu](https://github.com/antfu) for mocking apps swiftly. With **file-based routing**, **components auto importing**, **markdown support**, I18n, PWA and uses **Tailwind** v2 for UI. + +<pre v-highlightjs><code class="javascript"> +// syntax highlighting example +function vitesse() { + const foo = 'bar' + console.log(foo) +} +</code></pre> + +heck out the [GitHub repo](https://github.com/antfu/vitesse) for more details. diff --git a/src/pages/README.md b/src/pages/README.md new file mode 100644 index 0000000..929b7c9 --- /dev/null +++ b/src/pages/README.md @@ -0,0 +1,20 @@ +## File-based Routing + +Routes will be auto-generated for Vue files in this dir with the same file structure. +Check out [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) for more details. + +### Path Aliasing + +`~/` is aliased to `./src/` folder. + +For example, instead of having + +```ts +import { isDark } from '../../../../composables' +``` + +now, you can use + +```ts +import { isDark } from '~/composables' +``` diff --git a/src/pages/[...all].vue b/src/pages/[...all].vue new file mode 100755 index 0000000..241e7e2 --- /dev/null +++ b/src/pages/[...all].vue @@ -0,0 +1,5 @@ +<template> + <div> + Not Found + </div> +</template> diff --git a/src/pages/base.vue b/src/pages/base.vue new file mode 100644 index 0000000..0260305 --- /dev/null +++ b/src/pages/base.vue @@ -0,0 +1,50 @@ +<script setup lang="ts"> +const name = ref('') + +const router = useRouter() +const go = () => { + if (name.value) + router.push(`/hi/${encodeURIComponent(name.value)}`) +} +</script> + +<template> + <div> + <div i-carbon-campsite text-4xl inline-block /> + <p> + <a rel="noreferrer" href="https://github.com/antfu/vitesse-lite" target="_blank"> + Vitesse Lite + </a> + </p> + <p> + <em text-sm op75>Opinionated Vite Starter Template</em> + </p> + + <div py-4 /> + + <input + id="input" + v-model="name" + placeholder="What's your name?" + type="text" + autocomplete="false" + p="x-4 y-2" + w="250px" + text="center" + bg="transparent" + border="~ rounded gray-200 dark:gray-700" + outline="none active:none" + @keydown.enter="go" + > + + <div> + <button + class="m-3 text-sm btn" + :disabled="!name" + @click="go" + > + Go + </button> + </div> + </div> +</template> diff --git a/src/pages/hi/[name].vue b/src/pages/hi/[name].vue new file mode 100644 index 0000000..d8e4fcd --- /dev/null +++ b/src/pages/hi/[name].vue @@ -0,0 +1,25 @@ +<script setup lang="ts"> +const props = defineProps<{ name: string }>() +const router = useRouter() +</script> + +<template> + <div> + <div i-carbon-pedestrian text-4xl inline-block /> + <p> + Hi, {{ props.name }} + </p> + <p text-sm op50> + <em>Dynamic route!</em> + </p> + + <div> + <button + class="btn m-3 text-sm mt-8" + @click="router.back()" + > + Back + </button> + </div> + </div> +</template> diff --git a/src/router.ts b/src/router.ts new file mode 100644 index 0000000..be53f8e --- /dev/null +++ b/src/router.ts @@ -0,0 +1,56 @@ +// eslint-disable-next-line no-unused-vars +import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router' + +// import { defineAsyncComponent } from 'vue' +import generatedRoutes from 'virtual:generated-pages' + +import {has_config} from '~/hooks/config' +import {load_data_info} from '~/hooks/loads' + +import Home from '~/views/Home.vue' +// import About from '~/views/about.md' + +const check_config = async(to: any, _from: any, next: any) => { + if (await has_config()) { + next() + } else { + next('/noconfig') + } +} + +const routes: RouteRecordRaw[] = [ + { + path: '/', + name: 'Home', + component: Home, + meta: { + requireAuth: false, + layout: 'AppLayout', + withauth: false, + }, + beforeEnter: [check_config, load_data_info ] + }, + { + path: '/:ky', + name: 'HomeKey', + component: Home, + meta: { + requireAuth: false, + layout: 'AppLayout', + withauth: false, + }, + beforeEnter: [check_config, load_data_info ] + }, + { + path: '/:catchAll(.*)', + name: '404', + meta: { layout: 'Page404' }, + component: () => import('./views/404.vue'), + }, + ...generatedRoutes, +] +const router: any = createRouter({ + history: createWebHistory(), + routes, +}) +export default router diff --git a/src/shims.d.ts b/src/shims.d.ts new file mode 100644 index 0000000..b8fd73a --- /dev/null +++ b/src/shims.d.ts @@ -0,0 +1,60 @@ +/* eslint-disable import/no-duplicates */ + +declare const ASSETS_PATH: string +declare const DATA_PATH: string +declare interface Window { + // extend the window + ROOT_LOCATION: string + CONFIG_LOCATION: string +} + +// with vite-plugin-md, markdowns can be treat as Vue components +declare module '*.md' { + import { ComponentOptions } from 'vue' + const component: ComponentOptions + export default component +} +/* +declare module '*.vue' { + import Vue from 'vue' + export default Vue +} +*/ +declare const _APP_VERSION: string +/* + * Keep states in the global scope to be reusable across Vue instances. + * + * @see {@link /createGlobalState} + * @param stateFactory A factory function to create the state +*/ +/* +declare function createGlobalState<T extends object>( + stateFactory: () => T +): () => T +*/ +interface EventTarget { + value: EventTarget|null + name: string| null + /** + * Appends an event listener for events whose type attribute value is type. The callback argument sets the callback that will be invoked when the event is dispatched. + * + * The options argument sets listener-specific options. For compatibility this can be a boolean, in which case the method behaves exactly as if the value was specified as options's capture. + * + * When set to true, options's capture prevents callback from being invoked when the event's eventPhase attribute value is BUBBLING_PHASE. When false (or not present), callback will not be invoked when event's eventPhase attribute value is CAPTURING_PHASE. Either way, callback will be invoked if event's eventPhase attribute value is AT_TARGET. + * + * When set to true, options's passive indicates that the callback will not cancel the event by invoking preventDefault(). This is used to enable performance optimizations described in Ā§ 2.8 Observing event listeners. + * + * When set to true, options's once indicates that the callback will only be invoked once after which the event listener will be removed. + * + * The event listener is appended to target's event listener list and is not appended if it has the same type, callback, and capture. + */ + addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void + /** + * Dispatches a synthetic event event to target and returns true if either event's cancelable attribute value is false or its preventDefault() method was not invoked, and false otherwise. + */ + dispatchEvent(event: Event): boolean + /** + * Removes the event listener in target's event listener list with the same type, callback, and options. + */ + removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void +} diff --git a/src/styles/main.css b/src/styles/main.css new file mode 100755 index 0000000..4d8b4e9 --- /dev/null +++ b/src/styles/main.css @@ -0,0 +1,254 @@ +html, +body, +#app { + height: 100%; + margin: 0; + padding: 0; +} + +html.dark { + background: #27272a; /*bg-gray-800*/ + color: #f0f0f0; + currentColor: #f0f0f0; +} +.right-item ul li { + margin-left: 1em; +} +.features li b { + color: #6366F1; /* indigo-600 */ + font-weight: 300; +} +.dark .features li b { + color: #A5B4FC; /* indigo-400 */ +} +li a, link, +.task a,h3 a,.link a { + color: #4F46E5; /*indigo-600*/ + text-decoration: underline; +} +.dark li a,.dark link, +.dark .task a,.dark h3 a,.dark .link a { + color: #A5B4FC; /* indigo-400 */ +} +.project-purpose, .project-name { + width: 100%; + padding: 4px 11px; + border-radius: 20px; + margin-bottom: 8px; + border: 1px solid #A5B4FC; /* indigo-400 */ + background-color: #F3F4F6; /* gray-100 */ + color: #6366F1; /* indigo-600 */ + font-size: 1.02em; + font-weight: 600; +} +.dark .project-purpose, .dark .project-name { + background-color: #374151; /* gray-500 */ + border: 1px solid #9CA3AF; /* gray-400 */ + color: #A5B4FC; /* indigo-400 */ +} +.dark svg { + fill: #f0f0f0; +} +.openbox { + border-radius: 10px; + border: 1px solid #374151; /* gray-500 */ +} +.dark .openbox { + border: 1px solid #F3F4F66; /* gray-100 */ +} +.msg-box { + border-radius: 40px !important; + border: 1px solid #374151; /* indigo-500 */ +} +.dark .msg-box { + border: 1px solid #F3F4F6; /* gray-100 */ +} + .msg-ok { + background: linear-gradient(135deg, #83D475, #2EB62C) !important; +} +.msg-error { + background: linear-gradient(135deg, #D90708, #9E1444) !important; +} +.msg-warn { + background: linear-gradient(135deg, #FDA766, #FD7F2C) !important; +} +.msg-show { + background: linear-gradient(135deg, #4949FF, #0000FF) !important; +} +.msg-info { + background: linear-gradient(135deg, #9E9E9E, #696969) !important; +} +.hljs { + background: none !important; +} +.markdown-body { + margin: 2em; +} +.markdown-body p { + margin-top: 1em;; + font-size: 1em; +} + +.markdown-body pre { + padding: 2em; + background: rgba(243,244,246); +} +.dark .markdown-body pre { + background: rgba(75, 85, 99,var(--tw-bg-opacity)) +} +.markdown-body a { + color: rgba(37, 99, 235); +} +.no-list { list-style-type: none; margin-top: 1em;} +/* +.btn { + @apply px-4 py-1 rounded inline-block + bg-teal-600 text-white cursor-pointer + hover:bg-teal-700 + disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50; +} + +.icon-btn { + @apply inline-block cursor-pointer select-none + opacity-75 transition duration-200 ease-in-out + hover:opacity-100 hover:text-teal-600; + font-size: 0.9em; +} +/* +.btn { + @apply px-4 py-1 rounded inline-block + bg-teal-600 text-white cursor-pointer + hover:bg-teal-700 + disabled:cursor-default disabled:bg-gray-600 disabled:opacity-50; +} + +.icon-btn { + @apply inline-block cursor-pointer select-none + opacity-75 transition duration-200 ease-in-out + hover:opacity-100 hover:text-teal-600; + font-size: 0.9em; +} +*/ +progress { + margin-top: 0.5rem; + width: 100%; + height: 10px; + margin-bottom: 0.4rem; +} + +progress::-webkit-progress-bar { + border-radius: 10px; + background-color: #d1d5db; + box-shadow: 0 2px 6px #555; +} + +progress::-webkit-progress-value { + border-radius: 10px 0 0 10px; + background-image: linear-gradient(36deg, #d1fae5, #818cf8); +} +#dinoeatlife-headline::before { + display: inline-block; + height: 30px; + width: 30px; + position: relative; + bottom: -8px; + content: ""; + background-image: url("/images/assets/dinoeatlife-logo.png"); + background-position: center; + background-size: contain; + background-repeat: no-repeat; + margin-right: 12px; +} + +#chrisko-headline::before { + display: inline-block; + height: 30px; + width: 30px; + position: relative; + bottom: -8px; + content: ""; + background-image: url("/images/assets/chrisko-icon.png"); + background-position: center; + background-size: contain; + margin-right: 12px; + border-radius: 50%; +} + +#mealio-headline::before { + display: inline-block; + height: 30px; + width: 30px; + position: relative; + bottom: -8px; + content: ""; + background-image: url("/images/assets/mealio-icon.png"); + background-position: center; + background-size: contain; + margin-right: 12px; +} + +#headsup-headine::before { + display: inline-block; + height: 30px; + width: 30px; + position: relative; + bottom: -4px; + content: ""; + background-image: url("/images/assets/headsup-logo.svg"); + background-position: center; + background-size: contain; + background-repeat: no-repeat; + margin-right: 12px; +} + +.m-ul { + margin-top: 0 !important; +} + +.m-ul li:first-child { + margin-top: 0 !important; +} +.prose>:first-child { + margin-top: 0 !important; +} +.prose h2 { + font-size: 1.5em; + font-weight: 500; + line-height: 1.3333333; + /* margin-bottom: 1em; */ + /* margin-top: 2em; */ + text-transform: uppercase; +} +#cv-wrapper { + box-shadow: rgba(0, 0, 0, 0.16) 0px 10px 36px 0px, rgba(0, 0, 0, 0.06) 0px 0px 0px 1px; + /* box-shadow: rgba(50, 50, 93, 0.25) 0px 50px 100px -20px, rgba(0, 0, 0, 0.3) 0px 30px 60px -30px, rgba(10, 37, 64, 0.35) 0px -2px 6px 0px inset; */ + /* https://getcssscan.com/css-box-shadow-examples */ +} +div strong, p strong, .itm-title { + color: #6366F1; /* indigo-600 */ + font-weight: 600; +} +.dark div strong, .dark p strong, .dark .itm-title { + color: #A5B4FC; /* indigo-400 */ + font-weight: 600; +} +img.project { + width: 10em; + max-height: 5em; + } +@media print { + * { + line-height: 1em !important; + } + .main-container { max-width: fit-content !important;} + .noprint { + visibility: hidden; + display: none; + margin-top: 1em; + } + #cv-wrapper { margin-top: 0 !important;} + .page-break { display: block; break-before: auto; } +} +/* @media all { +.page-break { display: none; } +} */ \ No newline at end of file diff --git a/src/typs/clouds/index.ts b/src/typs/clouds/index.ts new file mode 100644 index 0000000..270d865 --- /dev/null +++ b/src/typs/clouds/index.ts @@ -0,0 +1,86 @@ +export const enum LanguageType { + en = 'en', + es = 'es', + None = 'None', +} +export interface StatusItemType { + title: string + content: string + lang: LanguageType + datetime: string + isOpen: boolean +} +export interface StatusItemDataType { + [key: string]: any + title: string + content: string + lang: LanguageType + datetime: string +} +export const enum ReqType { + tcp = 'tcp', + https = 'https', + NotSet = 'NotSet', +} +export const enum CriticalType { + yes = 'yes', + cloud = 'cloud', + group = 'group', + ifresized = 'ifresized', + no = 'no', +} +export interface SrvcType { + name: string + path: string + req: ReqType + target: string + liveness: string + critical: CriticalType +} +export interface SrvcInfoType { + [key: string]: any + name: string + info: string + srvc: SrvcType +} +export interface CloudGroupItemType { + [key: string]: any + hostname: string + tsksrvcs: SrvcType[] + appsrvcs: SrvcType[] +} +export interface CloudGroupServcType { + [key: string]: any + hostname: string + name: string + tsksrvcs: SrvcInfoElemType[] + appsrvcs: SrvcInfoElemType[] +} + +export interface CloudDataCheck { + [key: string]: any + name: string + apps: Map<string, Map<string, CloudGroupServcType>> + cloud: Map<string, Map<string, CloudGroupServcType>> + infos: StatusItemType[] +} +export interface CloudOptionType { + name: string + option: number +} +export interface CloudGroupDataType { + [key: string]: CloudGroupItemType[] | any +} +export interface ResCloudDataCheck { + [key: string]: any + name: string + cloud: CloudGroupDataType + apps: CloudGroupDataType + // cloud: CloudGroupSrvcType + // statusentries: StatusItemDataType[] +} +export interface ResCloudDataCheckDefs { + [key: string]: any + check: ResCloudDataCheck[] + defs: any +} \ No newline at end of file diff --git a/src/typs/cmpnts/index.ts b/src/typs/cmpnts/index.ts new file mode 100644 index 0000000..91342c0 --- /dev/null +++ b/src/typs/cmpnts/index.ts @@ -0,0 +1,59 @@ + +export const enum Profile { + Premium, + Basic, + Pro, + NotSet, +} + +export enum Auth { + Allow, + Denied, + NotSet, +} + +export enum Permission { + Read, + Write, + ReadWrite, + NotSet, +} + +export interface DataContext { + profile: Profile + auth: Auth + perms: Permission +} + +export interface AccordionItemType { + title?: string + status: boolean + h: number +} + +export interface SideMenuItemType { + title: string + ctx?: string + mode?: string + name?: string + icon_on?: string + img?: string + name_to?: string + show_to: boolean + path?: string + click?: string + type: NavItemType + pfx?: string + href?: string + label?: string + vif?: string + active?: boolean + cllbck?: Function | null +} + +export interface UiPanelsType { + [key: string]: any + id: string + style: string +} + diff --git a/src/typs/config.ts b/src/typs/config.ts new file mode 100644 index 0000000..744284d --- /dev/null +++ b/src/typs/config.ts @@ -0,0 +1,4 @@ + +export interface ConfUrlsType { + [key: string]: string +} diff --git a/src/typs/cv.ts b/src/typs/cv.ts new file mode 100644 index 0000000..3ab5d4e --- /dev/null +++ b/src/typs/cv.ts @@ -0,0 +1,320 @@ +export interface AuthInfoType { + editable: boolean + viewchange: boolean + show: boolean +} +export interface SkillsType { + id: string + auth: AuthInfoType + title: string + max: number + value: number +} +export interface CertificationType { + id: string + auth: AuthInfoType + title: string + author: string + date: string + link: string + href: string + certid: string +} +export interface SitesType { + id: string + auth: AuthInfoType + title: string + sub: string + link: string + type: string + alt: string + img: string +} +export interface LangsType { + id: string + title: string + mode: string +} +export interface WorkExperienceType { + [key: string]: any + auth: AuthInfoType + date: string + where: string + wheredef: string + location: string + position: string + description: string + tools: string[] + tasks: string[] +} +export interface TalksType { + [key: string]: any + auth: AuthInfoType + date: string + title: string + org: string + location: string + description: string[] +} +export interface TeachingType { + [key: string]: any + auth: AuthInfoType + date: string + title: string + org: string + location: string + description: string[] +} +export interface EducationType { + [key: string]: any + auth: AuthInfoType + date: string + title: string + org: string + location: string + cert: string + description: string[] + tools: string[] +} +export interface ProjectType { + [key: string]: any + auth: AuthInfoType + date: string + name: string + title: string + img: string + site: string + code: string + purpose: string + for: string + position: string + license: string + demo: string + capture: string + description: string + features: string[] + builtwith: string[] +} +export interface ProfileType { + [key: string]: unknown; + auth: AuthInfoType + desc: string +} +export interface OtherType { + [key: string]: unknown; + auth: AuthInfoType + desc: string +} +export interface MissionHowType { + [key: string]: unknown; + auth: AuthInfoType + desc: string +} +export interface DataCoreType { + [key: string]: any + name: string + fullname: string + title1: string + title2: string + imgalt: string + imgsrc: string + email: string + phone: string + address: string + postalcode: string + state: string + city: string + country: string + birthdate: string + status: string + mission: string + mission_how: MissionHowType[] + profile: ProfileType[] + certifications: CertificationType[] + skills: SkillsType[] + infra: SkillsType[] + sites: SitesType[] + langs: LangsType[] +} + +export interface DataLangType { + [key: string]: any + imgalt: string + title1: string + title2: string + country: string + birthdate: string + status: string + mission: string + mission_how: MissionHowType[] + profile: ProfileType[] + certifications: CertificationType[] + work_experiences: WorkExperienceType[] + projects: ProjectType[] + education: EducationType[] + talks: TalksType[] + teaching: TeachingType[] + others: OtherType[] +} +export interface DataLangsType { + [key: string]: DataLangType +} +export interface ShowTalksType { + [key: string]: unknown; + auth: AuthInfoType + date: boolean + title: boolean + org: boolean + location: boolean + description: boolean +} +export interface ShowTeachingType { + [key: string]: unknown; + auth: AuthInfoType + date: boolean + title: boolean + org: boolean + location: boolean + description: boolean +} +export interface ShowWorkExperienceType { + [key: string]: unknown; + auth: AuthInfoType + date: boolean + where: boolean + wheredef: boolean + location: boolean + position: boolean + description: boolean + tools: boolean + tasks: boolean +} +export interface ShowEducationType { + [key: string]: unknown; + auth: AuthInfoType + date: boolean + title: boolean + org: boolean + location: boolean + cert: boolean + description: boolean + tools: boolean +} +export interface ShowProjectType { + [key: string]: unknown; + auth: AuthInfoType + name: boolean + img: boolean + title: boolean + purpose: boolean + site: boolean + code: boolean + date: boolean + for: boolean + position: boolean + license: boolean + demo: boolean + capture: boolean + description: boolean + features: boolean + builtwith: boolean +} +export interface ShowInfoType { + [key: string]: any + id: string + ky: string + auth: AuthInfoType + write: boolean + change: boolean + admin: boolean + // save: boolean + fullname: boolean + personal: boolean + title: boolean + image: boolean + mission: boolean + mission_how: boolean + phone: boolean + address: boolean + status: boolean + birthdate: boolean + sites: boolean + skills: boolean + skills_itms: boolean + infra: boolean + certs: boolean + langs: boolean + profile: boolean + work_experience_itms: boolean + work_experience: ShowWorkExperienceType + project_itms: boolean + projects: ShowProjectType + education_itms: boolean + education: ShowEducationType + talks_itms: boolean + talks: ShowTalksType + teaching_itms: boolean + teaching: ShowTeachingType + others_itms: boolean + others: boolean +} + +export interface HtmlAttrsType { + [key: string]: any + bold: string + list: string + text: string + link: string +} +export interface OptionsSelectorType { + title: string + val: string +} +export interface InputBtnsType { + id: string + title: string + typ: string + show: boolean +} +export enum MessageBoxType { + Save = 'save', + Select = 'select', + Input = 'input', + OneInput = 'oneinput', + NotSet = '', +} +export enum NavPosition { + header = 'header', + footer = 'footer', +} + +export interface ModelType { + [key: string]: any + id: string + title: string + path: string +} +export interface ModelSelType { + [key: string]: ModelType +} +export interface CVDataType { + models: ModelType[] + showinfo: ShowInfoType[] + core: DataCoreType[] + work_experience: WorkExperienceType[] + projects: ProjectType[] + education: EducationType[] + talks: TalksType[] + teaching: TeachingType[] + others: OtherType[] +} +export interface CVLangDataType { + [key: string]: CVDataType +} +export interface CVModelDataType { + [key: string]: CVLangDataType +} +export interface CVPostDataType { + u: string + data: CVModelDataType +} \ No newline at end of file diff --git a/src/typs/index.ts b/src/typs/index.ts new file mode 100644 index 0000000..406a495 --- /dev/null +++ b/src/typs/index.ts @@ -0,0 +1,33 @@ +export enum LocalesLabelModes { + auto = 'auto', + value = 'val', + translation = 'trans', +} + +export enum MessageType { + Show, + Success, + Error, + Warning, + Info, +} +export interface MenuItemType { + id: string + title: string + active: boolean + link: string +} + +export interface DataSections { + [key: string]: string +} + +export enum NavItemType { + router_link = 'router_link', + app_link = 'app_link', + a_blank = 'a_blank', + a_link = 'a_link', + cloud_link = 'cloud_link', + module_label = 'module_label', + separator = 'separator', +} diff --git a/src/views/404.vue b/src/views/404.vue new file mode 100644 index 0000000..0b9eb9a --- /dev/null +++ b/src/views/404.vue @@ -0,0 +1,28 @@ +<script setup lang="ts"> +import { useRouter } from 'vue-router' +import { useI18n } from 'vue-i18n' + +const router = useRouter() +const { t } = useI18n() +</script> + +<template> + <main class="px-4 py-10 text-center text-teal-700 dark:text-gray-200"> + <div> + <p class="text-4xl"> + <carbon-warning class="inline-block" /> + </p> + </div> + <transition name="fade"> + <router-view /> + </transition> + <div> + <button + class="btn m-3 text-sm mt-8" + @click="router.back()" + > + {{ t('button.back') }} + </button> + </div> + </main> +</template> diff --git a/src/views/Home.vue b/src/views/Home.vue new file mode 100644 index 0000000..64086a9 --- /dev/null +++ b/src/views/Home.vue @@ -0,0 +1,584 @@ +<template> + <div + v-if="openMessageBox" + class="fixed inset-0 bg-gray-600 overflow-y-auto h-full w-full z-80 dark:bg-gray-900 opacity-97" + /> + <message-box-view + :messageType="message_type" + :openMessageBox="openMessageBox" + :show_input="show_input" + :input_placeholder="input_placeholder" + :input_btns="input_btns" + :select_ops="select_ops" + :data_url_encoded="data_url_encoded" + :inpType="oneInputType" + @onInput="onInput" + @onMessageBox="onMessageBox" + @onLoadModel="onLoadModel" + /> + <div v-if="show_content && cvdata.core && cvdata.core.name" class="font-sans antialiased"> + <div class="container mx-auto max-w-screen-xl main-container"> + <nav-menu + :position="NavPosition.header" + :openMessageBox="openMessageBox" + :fixMenu="fixMenu" + :show_infopanel="show_infopanel" + :showinfo="show_info" + :authinfo="auth_info" + :needSave="needSave" + prefix='cv' + @onNavMenu="onNavMenu" + /> + <main + id="cv-wrapper" + class="rounded flex flex-col sm:flex-row-reverse shadow-2xl" + :class="{ 'mt-11': fixMenu }" + > + <div + v-if="show_infopanel && data_info.core && data_info.core.title1" + id="sidebar" + class="rounded-r w-full lg:w-80 sm:max-w-sm p-8 border-l-1 border-indigo-200 bg-gradient-to-b from-indigo-300 via-indigo-200 to-indigo-100 dark:bg-gray-600 dark:from-indigo-500 dark:via-indigo-400 dark:to-indigo-800" + > + <info-panel + :data="data_info.core" + :localedata="localedata" + :showinfo="show_info" + :authinfo="auth_info" + @onItem="onItem" + @onEditor="onEditor" /> + </div> + <div class="content w-full p-5"> + <div class="prose"> + <h2 + :class="show_info.profile ? 'section-headline' : `noprint ${auth_info.viewchange ? 'pb-11' : ''}`" + > + <span v-if="show_info.profile">{{ t('cv.profile', 'Profile') }}</span> + <button + v-if="auth_info.viewchange" + class="no-print text-sm float-right icon-btn mt-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('profile')" + > + <div v-if="show_info.profile" i-carbon-view-off /> + <div v-else class="noprint flex"> + <div class="-mt-0.5 mr-2 line-through">{{ t('cv.profile', 'Profile') }}</div> + <div i-carbon-view /> + </div> + </button> + </h2> + <profile + v-if="show_info.profile" + :data="data_info.core.profile" + :localedata="localedata" + :showinfo="show_info" + :authinfo="auth_info" + @onEditor="onEditor" + @onItem="onItem" + /> + <hr v-if="show_info.profile" class="hr-sep" /> + </div> + <div v-for="(sec,indexsec) in data_sections" :key="sec"> + <div + :id="`${prefix}-${sec}`" + class="prose" + :class="{ 'page-break': indexsec > 0 && show_info[`${sec}_itms`] }" + > + <h2 + :class="show_info[`${sec}_itms`] ? 'section-headline' : `noprint ${auth_info.viewchange ? 'pb-11' : ''}`" + > + <span v-if="show_info[`${sec}_itms`]">{{ t(`cv.${sec}`, sec) }}</span> + <button + v-if="auth_info.viewchange" + class="no-print text-sm float-right icon-btn mt-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView(`${sec}_itms`)" + > + <div v-if="show_info[`${sec}_itms`]" i-carbon-view-off /> + <div v-else class="noprint flex"> + <div class="-mt-0.5 mr-2 line-through">{{ t(`cv.${sec}`, sec) }}</div> + <div i-carbon-view /> + </div> + </button> + <button + v-if="show_info[`${sec}_itms`]" + class="no-print text-sm float-right icon-btn mt-2 mr-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onHome()" + > + <div i-carbon-home /> + </button> + </h2> + <projects-view + v-if="sec === 'projects' && show_info[`${sec}_itms`]" + :data="data_info[sec]" + :localedata="localedata" + :showinfo="show_info[sec]" + @onEditor="onEditor" + @onItem="onItem" + /> + <work-experience-view + v-if="sec === 'work_experience' && show_info[`${sec}_itms`]" + :data="data_info[sec]" + :localedata="localedata" + :showinfo="show_info[sec]" + @onEditor="onEditor" + @onItem="onItem" + /> + <education-view + v-if="sec === 'education' && show_info[`${sec}_itms`]" + :data="data_info[sec]" + :localedata="localedata" + :showinfo="show_info[sec]" + @onEditor="onEditor" + @onItem="onItem" + /> + <teaching-view + v-if="sec === 'teaching' && show_info[`${sec}_itms`]" + :data="data_info[sec]" + :localedata="localedata" + :showinfo="show_info[sec]" + @onEditor="onEditor" + @onItem="onItem" + /> + <talks-view + v-if="sec === 'talks' && show_info[`${sec}_itms`]" + :data="data_info[sec]" + :localedata="localedata" + :showinfo="show_info[sec]" + @onEditor="onEditor" + @onItem="onItem" + /> + <others-view + v-if="sec === 'others' && show_info[`${sec}_itms`]" + :data="data_info.others" + :localedata="localedata" + :showinfo="show_info" + @onEditor="onEditor" + @onItem="onItem" + /> + <hr v-if="show_info[`${sec}_itms`] && indexsec > -1" class="hr-sep mt-11" /> + </div> + </div> + <div + v-if="!show_infopanel" + id="cv-skills" + class="prose" + > + <h2 class="section-headline"> + <span v-if="show_info.skills">{{ t('cv.skills_tools', 'Skill & Tools') }}</span> + <button + v-if="auth_info.viewchange" + class="no-print text-sm float-right icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onSkills()" + > + <div v-if="show_info.skills" i-carbon-view-off /> + <div v-else class="noprint flex-grow-0 flex -mb-5"> + <div class="-mt-0.5 mr-2 line-through text-xs">{{ t('cv.skills_tools', 'Skill & Tools') }}</div> + <div i-carbon-view /> + </div> + </button> + <button + v-if="show_info.skills" + class="no-print text-sm icon-btn mr-2 float-right !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onHome()" + > + <div i-carbon-home /> + </button> + </h2> + <div v-if="show_info.skills" class="list-none w-3/5"> + <skills-view + :data="data_info.core.skills" + :localedata="localedata" + :showinfo="show_info.skills" + :authinfo="show_info.auth" /> + </div> + </div> + </div> + </main> + <div + v-if="show_content && cvdata.core && cvdata.core.name" + class="mr-auto w-full lg:w-1/2 text-center text-sm py-2 pr-5 text-gray-600 border-gray-300 border-1 border-b-0 rounded-t-lg" + > + <nav-menu + :position="NavPosition.footer" + :fixMenu="fixMenu" + :show_infopanel="show_infopanel" + :showinfo="show_info" + :authinfo="auth_info" + :needSave="needSave" + prefix='cv' + :openMessageBox="openMessageBox" + @onNavMenu="onNavMenu" + /> + </div> + </div> + </div> + <div v-else class="text-center m-auto"> + <img v-if="isDark" + class="m-auto mt-5" + width="250" + :src="`${assetsPath}/images/cvgen_b.svg`" + /> + <img v-else + class="m-auto mt-5" + width="250" + :src="`${assetsPath}/images/cvgen_w.svg`" + /> + <h2 class="mt-8 text-2xl text-gray-700 dark:text-gray-400">{{ t('message.loading', 'Loading') }}...</h2> + <h3> {{ current_model.title || '' }}</h3> + <h4>{{useState().connection.value.state}}</h4> + </div> +</template> +<script setup lang="ts"> +import { useI18n } from 'vue-i18n' +const i18n = useI18n() +const { t } = useI18n() +const router = useRouter() + +import InfoPanel from '~/views/cv/InfoPanel.vue' +import Profile from '~/views/cv/Profile.vue' +import WorkExperienceView from '~/views/cv/WorkExperience.vue' +import ProjectsView from '~/views/cv/Projects.vue' +import TeachingView from '~/views/cv/Teaching.vue' +import TalksView from '~/views/cv/Talks.vue' +import EducationView from '~/views/cv/Education.vue' +import SkillsView from '~/views/cv/Skills.vue' +import OthersView from '~/views/cv/Others.vue' +import MessageBoxView from '@/MessageBoxView.vue' +import NavMenu from '@/NavMenu.vue' +import { isDark } from '~/composables' +// import Modal from '@/Modal.vue' +import useState from '~/hooks/useState' +import { parseJwt,checkUserAuth } from '~/hooks/utilsAuth' +import { send_data, show_message } from '~/hooks/utils' +import { load_data_model,load_currentRoute } from '~/hooks/loads' +import { track_action } from '~/hooks/tracking' +import { MessageType} from '~/typs' +import { MessageBoxType, NavPosition, InputBtnsType, ModelType, ShowInfoType } from '~/typs/cv' + +const prefix = 'cv' +const assetsPath = useState().ASSETS_PATH +const routeKy = router.currentRoute.value.params.ky || router.currentRoute.value.query.k || '' +const message_type = ref(MessageBoxType.NotSet) +const fixMenu = ref(false) +const show_content = ref(true) +const show_infopanel = ref(true) +const openMessageBox = ref(false) +const needSave = ref(false) +const show_input = ref(false) +const data_url_encoded = ref('') +const input_btns = ref([] as InputBtnsType[]) +const input_placeholder = ref('') +const oneInputType = ref('') +const localedata = computed(() => { + return useState().datalang.value[i18n.locale.value] ? useState().datalang.value[i18n.locale.value] : undefined +}) +const onInput = (btn: InputBtnsType) => { + switch (btn.id) { + case 'ok': + break + case 'cancel': + break + } + show_input.value = false +} +const current_model = useState().current_model +const select_ops = computed(() => { + if (useState().current_modelid.value) { + return Object.fromEntries(Object.entries(useState().models.value).filter(([key]) => key !== useState().current_modelid.value)) as ModelType + } + return useState().models.value +}) +const cvdata = useState().cvdata +const show_info = computed(() => { + let showinfo = useState().showinfo.value + switch (i18n.locale.value) { + case 'es': + showinfo = { + ...useState().showinfo.value, + fullname: true, + } + break + default: + break + } + useState().authinfo.value = showinfo.auth ? showinfo.auth : { editable: false, viewchange: false, show: true } + return showinfo +}) +const auth_info = useState().authinfo +const data_info = useState().cvdata +const data_sections = useState().dataSections +const refresh = () => { + show_content.value = false + setTimeout(() => show_content.value = true, 1) +} +const onItem = (data: { root: string, src: string, _itm: any, idx: number }) => { + switch(data.root) { + case 'info': + const source = useState().datalang.value[i18n.locale.value] ? 'corelang': 'core' + if (source === 'corelang') { + if (data.idx > -1) { + useState().datalang.value[i18n.locale.value][source][data.src][data.idx].auth.show = !useState().datalang.value[i18n.locale.value][source][data.src][data.idx].auth.show + } else { + useState().datalang.value[i18n.locale.value][source][data.src].auth.show = !useState().datalang.value[i18n.locale.value][source][data.src].auth.show + } + } else { + if (data.idx > -1) { + useState().cvdata.value[source][data.src][data.idx].auth.show = !useState().cvdata.value[source][data.src][data.idx].auth.show + } else { + useState().cvdata.value[source][data.src].auth.show = !useState().cvdata.value[source][data.src].auth.show + } + } + break + default: + if (data.src !== '' && data.idx > -1) { + if (useState().datalang.value[i18n.locale.value] && useState().datalang.value[i18n.locale.value][data.src] && useState().datalang.value[i18n.locale.value][data.src][data.idx]) { + useState().datalang.value[i18n.locale.value][data.src][data.idx].auth.show = !useState().datalang.value[i18n.locale.value][data.src][data.idx].auth.show + } else if (useState().cvdata.value[data.src] && useState().cvdata.value[data.src][data.idx]) { + useState().cvdata.value[data.src][data.idx].auth.show = !useState().cvdata.value[data.src][data.idx].auth.show + } + } + } +} +const onView = (itm: string) => { + if (typeof useState().showinfo.value[itm] === 'boolean') + useState().showinfo.value[itm] = !useState().showinfo.value[itm] +} +const onNavMenu = (item: { src: string, target: string }) => { + switch (item.src) { + case 'locale': + track_action(null, { ref: 'locale',text: i18n.locale.value}) + refresh() + break + case 'infopanel': + track_action(null, { ref: 'onNavMenu',text: 'item.src'}) + show_infopanel.value = !show_infopanel.value + break + case 'fixmenu': + track_action(null, { ref: 'onNavMenu',text: 'item.src'}) + fixMenu.value = !fixMenu.value + break + case 'endinput': + show_input.value = false + break + case 'select': + track_action(null, { ref: 'onNavMenu',text: 'item.src'}) + message_type.value = MessageBoxType.Select + openMessageBox.value = true + break + case 'save': + track_action(null, { ref: 'onNavMenu',text: 'item.src'}) + input_placeholder.value = `${t('name', 'Name')} o 'data'` + show_input.value = true + message_type.value = MessageBoxType.Save + data_url_encoded.value = '' + openMessageBox.value = true + break + case 'editable': + track_action(null, { ref: 'onNavMenu',text: 'item.src'}) + useState().authinfo.value.editable = !useState().authinfo.value.editable + const state = useState().authinfo.value.editable ? 'on' : 'off' + const msgTyp = state === 'on' ? MessageType.Warning : MessageType.Info + show_message(msgTyp, `${t('saveload.editMode','Edit Mode')} ${t('cv.'+state,'')}`) + break + case 'viewchange': + track_action(null, { ref: 'onNavMenu',text: 'item.src'}) + useState().authinfo.value.viewchange = !useState().authinfo.value.viewchange + break + case 'goto': + const dom_id = document.getElementById(item.target) + if (dom_id) { + track_action(null, { ref: 'gotp',text: item.target}) + dom_id.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) + setTimeout(() => window.scrollBy(0, -40), 4000) + } + break + } +} +const saveData = async(mode: string,val: string) => { + const showinfo: ShowInfoType[] = useState().cvdata.value.showinfo + message_type.value = MessageBoxType.Save + const payload = parseJwt(localStorage.getItem(useState().AUTHKEY.value)) + const cv: any = { + u: payload.id ? payload.id : '', + data: {} + } + cv.data[val]={} + Object.keys(useState().datalang.value).forEach(lng => + cv.data[val][lng] = useState().datalang.value[lng] + ) + cv.data[val].main = useState().cvdata.value + showinfo.forEach((it,idx) => { + if (it.ky === useState().showinfo.value.ky) { + cv.data[val].main[idx] = useState().showinfo.value + } + }) + const url = useState().CONFURLS.value.send || '' + if (mode == 'send' && url !== '') { + onMessageBox({src: 'done', val: ''}) + const res = await send_data(url, cv, true, true, () => { + track_action(null, { ref: 'saveData',text: `${url} -> ${val}`}) + show_message(MessageType.Success, `${t('saveload.dataSaved','Data saved')}`,2000,() => { + }) + }) + console.log(res) + } else { + track_action(null, { ref: 'saveData',text: `local_json -> ${val}`}) + show_message(MessageType.Warning, `${t('saveload.saveData','Save data')}`,2000,() => { + data_url_encoded.value = `text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(cv))}` + }) + } +} +const onMessageBox = (item: { src: string, val: string }) => { + switch (item.src) { + case 'savedata': + show_input.value = false + saveData('local', item.val) + break + case 'sendata': + show_input.value = false + saveData('send', item.val) + break + case 'oneinput': + const r = async() => { + if (await checkUserAuth(item.val)) { + oneInputType.value='text' + openMessageBox.value = false + if (!await load_currentRoute(router.currentRoute.value)) { + show_message(MessageType.Error, `${t('saveload.loaderror','Load error')}`,2000) + } + } else { + show_message(MessageType.Error, `${t('saveload.autherror','Auth error')}`,2000) + } + } + r() + break + case 'endinput': + show_input.value = false + break + case 'done': + openMessageBox.value = false + break + case 'open': + openMessageBox.value = true + break + } +} +const onHome = () => { + const dom_body = document.getElementsByTagName('body')[0] + if (dom_body) { + dom_body.scrollIntoView({ behavior: 'smooth', block: 'start', inline: 'nearest' }) + } +} +const onSkills = () => { + show_info.value.skills = !show_info.value.skills + useState().showinfo.value.skills_itms = show_info.value.skills + // !useState().showinfo.value.skills_itms +} +const onLoadModel = (model: { id: string}) => { + const url = useState().CONFURLS.value.data || '' + if (useState().models.value[model.id] && url.length > 0 ) { + load_data_model(model.id, url, routeKy as string,true,() => { + show_content.value = false + show_message(MessageType.Success, `${t('saveload.dataLoaded','Data loaded')}`,2000,() => { + show_content.value = true + }) + }) + } +} +const update_field = (root: string, field: string, idx: number, data: string | string[]) => { + if (useState().datalang.value[i18n.locale.value] && useState().datalang.value[i18n.locale.value][root]) { + if (idx > -1) { + if (useState().datalang.value[i18n.locale.value][root][idx]) { + useState().datalang.value[i18n.locale.value][root][idx][field] = data + } else if (useState().datalang.value[i18n.locale.value][root][field][idx]) { + useState().datalang.value[i18n.locale.value][root][field][idx] = data + } + } else { + useState().datalang.value[i18n.locale.value][root][field] = data + } + } else if (useState().cvdata.value[root]) { + if (idx > -1 ) { + if (useState().cvdata.value[root][idx]) { + useState().cvdata.value[root][idx][field] = data + } else if (useState().cvdata.value[root][field][idx]) { + useState().cvdata.value[root][field][idx] = data + } + } else { + useState().cvdata.value[root][field] = data + } + } +} +const onEditor = (item: { src: string, field: string, idx: number, data: string, arr_data: string[], ev: Event }) => { + if (item.src === '' || item.data === '') { + return + } + const arr_itm_src = item.src.split('.') + needSave.value = true + switch (arr_itm_src[0]) { + case 'info': + const source = useState().datalang.value[i18n.locale.value] ? 'corelang': 'core' + const src = arr_itm_src.length > 1 ? arr_itm_src[1] : item.field + switch (src) { + case 'profile': + case 'mission_how': + if (source === 'corelang' && useState().datalang.value[i18n.locale.value][source][src][item.idx][item.field]) { + useState().datalang.value[i18n.locale.value][source][src][item.idx][item.field] = item.data + } else if (useState().cvdata.value[source][src][item.idx][item.field]) { + useState().cvdata.value[source][src][item.idx][item.field] = item.data + } + break + default: + update_field(source, item.field, item.idx, item.data) + } + break + case 'projects': + switch (item.field) { + case 'features': + case 'builtwith': + update_field(item.src, item.field, item.idx, item.arr_data) + break + default: + update_field(item.src, item.field, item.idx, item.data) + } + break + case 'work_experience': + switch (item.field) { + case 'tasks': + case 'tools': + update_field(item.src, item.field, item.idx, item.arr_data) + break + default: + update_field(item.src, item.field, item.idx, item.data) + } + break + case 'education': + case 'teachings': + case 'talks': + switch (item.field) { + case 'tools': + case 'description': + update_field(item.src, item.field, item.idx, item.arr_data) + break + default: + update_field(item.src, item.field, item.idx, item.data) + } + break + case 'others': + if (item.idx > -1) { + if (useState().datalang.value[i18n.locale.value] && useState().datalang.value[i18n.locale.value][item.src] && useState().datalang.value[i18n.locale.value][item.src][item.idx]) { + useState().datalang.value[i18n.locale.value][item.src][item.idx].desc = item.data + } else if (useState().cvdata.value[item.src] && useState().cvdata.value[item.src][item.idx]) { + useState().cvdata.value[item.src][item.idx].desc = item.data + } + } + break + } +} +onBeforeMount(async() => { + if (!useState().allowView.value && useState().userID.value === "?") { + oneInputType.value='password' + message_type.value=MessageBoxType.OneInput + openMessageBox.value=true + show_input.value=false + input_placeholder.value='Enter password' + } +}) +</script> \ No newline at end of file diff --git a/src/views/cv/Education.vue b/src/views/cv/Education.vue new file mode 100644 index 0000000..6f7d490 --- /dev/null +++ b/src/views/cv/Education.vue @@ -0,0 +1,165 @@ +<template> + <div v-for="(item,index) in get_data('education')" :key="index" class="edu-item"> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="item.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through text-xs">{{ index }}</div> + </div> + </button> + </div> + <div v-if="item.auth.show" :id="`education-${index}`"> + <div v-for="info,key,infoindex in showinfo" :key="infoindex" class> + <section v-if="info && item[key] && key !== 'tools' && key !== 'description' && key !== 'auth'" class="flex mb-0"> + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item" :class="{ 'text-xs': key === 'cert' }"> + <span v-if="item[key].includes && item[key].includes('http')" class="link"> + <a + :href="item[key]" + rel="noopener noreferrer" + target="_blank" + >{{ item[key].split('://')[1] }}</a> + </span> + <span v-else> + <tiptap-editor + v-if="authinfo.editable" + :data="item[key]" + :editable="authinfo.editable" + src="education" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else>{{ item[key] }}</span> + </span> + </div> + </section> + <section + v-if="key === 'description' && item.description && Object.keys(item.description).length > 0" + class="mt-0" + > + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key, key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <ul class="list-circle"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<li>${item[key].join('</li><li>')}</li>`" + :editable="authinfo.editable" + src="education" + :field="key" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'itm-title font-normal' }" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="`<li>${item[key].join('</li><li>')}</li>`" /> + </ul> + </div> + </section> + <section + v-if="key === 'tools' && item.tools && Object.keys(item.tools).length > 0" + class="mb-2" + > + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t('tools', 'Tools').toLocaleLowerCase() }}</span> + </div> + <div class="tag-list right-item mt-1"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<b>${item[key].join('</b> <b>')}</b> `" + :editable="authinfo.editable" + src="projects" + :field="key" + :idx="index" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'tag-item font-light mb-2' }" + @onEditorBlur="onEditor" + /> + <span + v-else + v-for="tool,toolindex in item[key]" + :key="toolindex" + class="tag-item font-light mb-2" + :class="{ 'ml-2': toolindex > 0 }" + >{{ tool }}</span> + </div> + </section> + </div> + </div> + <hr v-if="item.auth.show && index < data.length - 1" class="hr-sep-itms" /> + </div> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import { EducationType, ShowEducationType,DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +const { t } = useI18n() +const props = defineProps({ + data: { + type: Array as PropType<EducationType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowEducationType>, + default: {}, + required: true, + }, +}) +const emit = defineEmits(['onEditor', 'onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value[itm] ? props.localedata.value[itm] : props.data +} +const authinfo = useState().authinfo.value +const onEditor = (info: { src: string, field: string, idx: number, data: string, ev: Event }) => { + let has_changed = false + let arr_data: string[] = [] + switch (info.field) { + case 'description': + arr_data = info.data.replace('<ul>', '').replace('</ul>', '').replace(/<li.+?>/g, '').split('</li>') + break + case 'tools': + arr_data = info.data.replace(/<strong.+?>/g, '').replace('<p>', '').replace('</p>', '').split('</strong>') + break + default: + has_changed = info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field] + } + if (arr_data.length > 0) { + arr_data = arr_data.filter(it => it !== '') + arr_data.forEach((it, idx) => { + if (it.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field][idx] && !has_changed) { + has_changed = true + } + }) + } + if (has_changed) { + emit('onEditor', { ...info, arr_data }) + } +} +const onItem = (idx: number) => { + emit('onItem', { src: 'education', itm: props.data[idx], idx }) +} +// const onItem = (e: any) => { +// const el = e.target && e.target.closest ? e.target.closest('.edu-item') : null +// if (el) +// el.style.display = 'none' +// } +</script> \ No newline at end of file diff --git a/src/views/cv/InfoPanel.vue b/src/views/cv/InfoPanel.vue new file mode 100644 index 0000000..0f3530a --- /dev/null +++ b/src/views/cv/InfoPanel.vue @@ -0,0 +1,476 @@ +<template> + <div class="px-1 mb-8"> + <div :class="{ 'flex': showinfo.image }"> + <img + v-if="showinfo.image" + :src="`${assets_path}${get_data('imgsrc')}`" + :alt="get_data('imgalt')" + class="flex-grow-0 rounded-full w-48 max-w-xs mx-auto mb-2 border-indigo-200 border-2" + /> + <button + v-if="authinfo.viewchange" + :class="showinfo.image ? 'flex-grow-0 mt-2' : 'float-right -mt-4'" + class="no-print text-sm h-6 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('image')" + > + <div i-carbon-view-off /> + </button> + </div> + <h1 + :class="showinfo.fullname ? 'text-lg' : 'text-2xl'" + class="text-center text-indigo-700 dark:text-indigo-200 font-semibold mb-2" + >{{ `${showinfo.fullname ? get_data('fullname') : get_data('name')}` }}</h1> + <h2 + v-if="showinfo.title && data.title1 !== ''" + class="text-center text-sm font-light text-indigo-500 dark:text-indigo-100" + > + <tiptap-editor + v-if="authinfo.editable" + :data="data.title1" + :editable="authinfo.editable" + src="info" + field="title1" + :idx="-1" + @onEditorBlur="onEditor" + /> + <span v-else v-html="data.title1" /> + </h2> + <h2 + v-if="showinfo.title && data.title2 !== ''" + class="text-center text-sm font-light text-indigo-500 dark:text-indigo-100" + > + <tiptap-editor + v-if="authinfo.editable" + :data="data.title2" + :editable="authinfo.editable" + src="info" + field="title2" + :idx="-1" + @onEditorBlur="onEditor" + /> + <span v-else v-html="data.title2" /> + </h2> + </div> + <div + v-if="data.mission !== ''" + class="font-light text-lg px-1" + :class="{ 'mb-8': showinfo.mission && authinfo.viewchange }" + > + <h2 class="text-center h2-title dark:text-indigo-900"> + <span v-if="showinfo.mission">{{ t('cv.mission', 'Mission') }}</span> + <button + v-if="authinfo.viewchange" + class="no-print text-sm float-right icon-btn mt-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('mission')" + > + <div v-if="showinfo.mission" i-carbon-view-off /> + <div v-else class="noprint flex -mt-7"> + <div class="-mt-0.5 mr-2 line-through">{{ t('cv.mission', 'Mission') }}</div> + <div i-carbon-view /> + </div> + </button> + </h2> + <p + v-if="showinfo.mission" + class="-ml-3 mb-2 text-xs font-semibold text-indigo-900 dark:text-indigo-200 tracking-normal" + > + <tiptap-editor + v-if="authinfo.editable" + :data="get_data('mission')" + :editable="authinfo.editable" + src="info" + field="title1" + :idx="-1" + @onEditorBlur="onEditor" + /> + <span v-else v-html="get_data('mission')" /> + </p> + <ul + v-if="showinfo.mission && showinfo.mission_how && get_data('mission_how').length > 0" + class="ml-2 list-disc" + > + <li + v-for="(mitem,index) in get_data('mission_how')" + :key="index" + class="text-xs tracking-normal font-light text-indigo-800 dark:text-gray-100" + :class="{ 'noprint list-none': !mitem.auth.show }" + > + <div class="flex"> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem('mission_how', index)" + > + <div v-if="mitem.auth.show" class="mr-1" i-carbon-view-off /> + <div v-else class="noprint flex flex-row"> + <div class="mt-0.2 mr-2 line-through text-xs">{{ index }}</div> + <div i-carbon-view /> + </div> + </button> + <div v-if=" mitem.auth.show"> + <tiptap-editor + v-if="authinfo.editable && mitem.auth.editable" + :data="mitem.desc" + :editable="authinfo.editable" + src="info.mission_how" + field="desc" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else>{{ mitem.desc }}</span> + </div> + </div> + </li> + </ul> + <button + v-if="authinfo.viewchange" + class="no-print text-sm float-right icon-btn mt-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('mission_how')" + > + <div v-if="showinfo.mission_how" i-carbon-view-off /> + <div v-else class="noprint flex -mt-7"> + <div class="-mt-0.5 mr-2 line-through">{{ t('cv.mission_how', 'Mission How') }}</div> + <div i-carbon-view /> + </div> + </button> + </div> + <div class="font-light text-lg px-1 mt-4 mb-8 panel-group"> + <h2 class="h2-title">{{ t('cv.contact', 'Contact') }}</h2> + <div class="flex items-center my-3"> + <img :src="`${assets_path}/images/assets/mail-outline.svg`" class="inline w-6 mr-4" alt="Mail icon" /> + <a @click="onLink" :href="`mailto:${data.email}`">{{ data.email }}</a> + </div> + <div + :class="{ 'hidden': !showinfo.phone && !authinfo.viewchange }" + class="flex items-center my-3" + > + <img + v-if="showinfo.phone" + :src="`${assets_path}/images/assets/call-outline.svg`" + class="inline w-6 mr-4" + alt="Phone icon" + /> + <a v-if="showinfo.phone" class="text-sm" @click="onLink" :href="`tel:${data.phone}`"> + <tiptap-editor + v-if="authinfo.editable" + :data="data.phone" + :editable="authinfo.editable" + src="info" + field="phone" + :idx="-1" + @onEditorBlur="onEditor" + /> + <span v-else>{{ data.phone }}</span> + </a> + <span v-if="!showinfo.phone" class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="no-print text-sm flex icon-btn ml-4 mt-0 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('phone')" + > + <div v-if="showinfo.phone" i-carbon-view-off /> + <div v-else class="noprint flex-grow-0 flex -mb-5"> + <div class="-mt-0.5 mr-2 line-through">{{ t('phone', 'Phone') }}</div> + <div i-carbon-view /> + </div> + </button> + </div> + <div + :class="{ 'hidden': !showinfo.address && !authinfo.viewchange }" + class="flex flex-col items-center my-5" + > + <img + v-if="showinfo.address" + :src="`${assets_path}/images/assets/home-outline.svg`" + class="flex-grow-0 w-6 pb-1 mr-4" + alt="House icon" + /> + <div v-if="showinfo.address" class="text-xs"> + <p>{{ data.address }}</p> + <p class="text-xs font-semibold">{{ get_data('city') }}</p> + <p>{{ data.postalcode }} {{ get_data('state') }}</p> + <p class="text-sm">{{ get_data('country')}}</p> + </div> + <span v-if="!showinfo.address" class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="no-print text-sm flex icon-btn ml-4 mt-0 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('address')" + > + <div v-if="showinfo.address" i-carbon-view-off /> + <div v-else class="noprint flex-grow-0 flex -mb-5"> + <div class="-mt-0.5 mr-2 line-through">{{ t('address', 'Adress') }}</div> + <div i-carbon-view /> + </div> + </button> + </div> + </div> + <div v-if="showinfo.birthdate || showinfo.status" class="panel-group"> + <h2 class="h2-title">{{ t('cv.personal', 'Personal') }}</h2> + <div v-if="showinfo.birthdate" class="flex items-center my-3"> + <img :src="`${assets_path}/images/assets/egg-outline.svg`" class="inline w-6 mr-4" alt="Egg Icon" /> + <span class="text-sm">{{ data.birthdate }}</span> + </div> + <div v-if="data.status && showinfo.status" class="flex items-center my-3"> + <img :src="`${assets_path}/images/assets/people-outline.svg`" class="inline w-6 mr-4" alt="Two Persons Icon" /> + <span>{{ t(`cv.${data.status.toLowerCase()}`, data.status) }}</span> + </div> + </div> + <div :class="{ 'hidden': !showinfo.sites && !authinfo.viewchange }" class="panel-group -mt-4"> + <h2 class="h2-title flex"> + <span v-if="showinfo.sites">{{ t('cv.onweb', 'On the Web') }}</span> + <span v-else class="w-5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="no-print text-sm flex icon-btn ml-2 mt-1.5 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('sites')" + > + <div v-if="showinfo.sites" i-carbon-view-off /> + <div v-else class="noprint flex-grow-0 flex -mb-5"> + <div class="-mt-0.5 mr-2 line-through text-xs">{{ t('cv.onweb', 'On the Web') }}</div> + <div i-carbon-view /> + </div> + </button> + </h2> + <div + v-if="showinfo.sites" + v-for="(site,index) in data.sites" + :key="site.id" + class="flex items-center y-3" + :class="{ 'noprint': !site.auth.show }" + > + <div v-if="authinfo.viewchange" class="flex-grow-0 flex noprint"> + <span v-if="!showinfo.sites" class="w-4/5 flex-grow" /> + <button + class="noprint text-xs mr-1 flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem('sites', index)" + > + <div v-if="site.auth.show" i-carbon-view-off /> + <div v-else class="noprint flex"> + <div class="-mt-0.5 mr-2 line-through">{{ index }}</div> + <div i-carbon-view /> + </div> + </button> + </div> + <div v-if="site.auth.show" class="flex-grow flex mb-2 text-sm"> + <img :src="`${assets_path}${site.img}`" class="flex-grow-0 inline w-6 mr-4" :alt="site.alt" /> + <span class="mt-0.5 mr-2 flex-grow-0 text-gray-500 dark:text-gray-700" style="font-size: 80%">{{ site.title }}</span> + <a @click="onLink" :href="site.link" target="_blank" rel="noopener noreferrer" class="flex-grow"> + <span class="flex-grow" style="font-size: 80%">{{ site.sub }}</span> + </a> + </div> + </div> + </div> + <div + id="info-skills" + :class="{ 'hidden': !showinfo.skills && !authinfo.viewchange }" + class="panel-group -mt-2" + > + <h2 class="h2-title flex"> + <span v-if="showinfo.skills">{{ t('cv.skills_tools', 'Skill & Tools') }}</span> + <div v-if="authinfo.viewchange" class="flex-grow-0 flex noprint ml-2"> + <span v-if="!showinfo.skills" class="w-4/5 flex-grow" /> + <button + class="no-print text-base flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('skills')" + > + <div v-if="showinfo.skills" class="flex" i-carbon-view-off /> + <div v-else class="noprint flex"> + <div class="mr-2 text-sm line-through">{{ t('cv.skills_tools', 'Skill & Tools').replaceAll(' ','') }}</div> + <div class="flex-grow-0" i-carbon-view /> + </div> + </button> + </div> + </h2> + <skills-view + v-if="showinfo.skills" + :data="data.skills" + :localedata="localedata" + :showinfo="showinfo" + :authinfo="authinfo" + @onItem="onSkills" + /> + </div> + <div v-if="showinfo.infra" class="panel-group"> + <h2 class="h2-title"> + <span>{{ t('cv.infrastructures', 'Infrastructures') }}</span> + <button + class="no-print text-sm float-right icon-btn mt-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('infra')" + > + <div i-carbon-view-off /> + </button> + </h2> + <div class="-ml-5 mt-2 leading-8 text-sm grid grid-flow-col grid-cols-2 grid-rows-2 gap-2"> + <div + v-for="infra in data.infra" + :key="infra.id" + class="border-1 border-indigo-400 rounded-xl bg-gray-300 w-25 px-3 ml-2" + >{{ infra.title }}</div> + </div> + </div> + <div :class="{ 'hidden': !showinfo.certs && !authinfo.viewchange }" class="panel-group -mt-4"> + <h2 class="h2-title flex"> + <span v-if="showinfo.certs">{{ t('cv.certifications', 'Certifications') }}</span> + <button + v-if="authinfo.viewchange" + class="no-print text-sm float-right icon-btn ml-2 mt-1 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('certs')" + > + <div v-if="showinfo.certs" i-carbon-view-off /> + <div v-else class="noprint flex flex-grow-0"> + <div class="-mt-0.5 mr-2 line-through">{{ t('cv.certifications', 'Certifications') }}</div> + <div class="flex-grow-0" i-carbon-view /> + </div> + </button> + </h2> + <div + v-if="showinfo.certs" + v-for="(cert,index) in get_data('certifications')" + :key="cert.id" + class="border-b-1 border-gray-400 mb-3 pb-2 flex" + > + <div v-if="cert.auth.show" class="flex-grow"> + <h3 class="font-semibold text-sm">{{ cert.title }}</h3> + <div class="font-light text-xs">{{ cert.date }}</div> + <div class="font-light text-xs flex flex-col"> + <div v-if="cert.href && cert.href !== ''" class="flex-grow"> + <small class="hidden">{{ t('link', 'Link') }}: </small> + <a @click="onLink" :href="cert.href">{{ cert.link }}</a> + </div> + <div v-if="cert.certid && cert.certid !== ''" class="flex-grow-0"> + <small>{{ t('id', 'Id') }}: {{ cert.certid }}</small> + </div> + </div> + <p v-if="cert.author && cert.author !== ''" class="font-light text-xs mb-2"> + <small class="hidden">{{ t('from', 'From') }}: </small> + <span class="font-semibold"> + <i>{{ cert.author }}</i> + </span> + </p> + <p v-else class="mb-2" /> + </div> + <div class="flex flex-grow-0"> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem('certifications', index)" + > + <div v-if="cert.auth.show" class="mr-1" i-carbon-view-off /> + <div v-else class="noprint flex flex-row"> + <div class="mt-0.2 mr-2 line-through text-xs">{{ index }}</div> + <div i-carbon-view /> + </div> + </button> + </div> + </div> + </div> + <div :class="{ 'hidden': !showinfo.langs && !authinfo.viewchange }" class="panel-group -mt-4"> + <h2 class="h2-title"> + <span v-if="showinfo.langs">{{ t('cv.languages', 'Languages') }}</span> + <span v-else class="w-5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="no-print text-sm float-right icon-btn mt-2 !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onView('langs')" + > + <div v-if="showinfo.langs" i-carbon-view-off /> + <div v-else class="noprint flex-grow-0 flex -mb-5"> + <div class="-mt-0.5 mr-2 line-through text-xs">{{ t('cv.languages', 'Languages') }}</div> + <div i-carbon-view /> + </div> + </button> + </h2> + <div v-if='showinfo.langs' + v-for="lang in data.langs" :key="lang.id" class="flex flex-row"> + <div class="font-semibold flex-grow">{{ t(`lang.${lang.title.toLowerCase()}`, lang.title) }}</div> + <div class="flex-grow-0 text-sm mt-1">{{ t(`lang.${lang.mode.toLowerCase()}`, lang.mode) }}</div> + </div> + </div> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { AuthInfoType, DataCoreType, ShowInfoType,DataLangsType } from '~/typs/cv' +import SkillsView from '~/views/cv/Skills.vue' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +import { track_action } from '~/hooks/tracking' +const emit = defineEmits(['onEditor', 'onItem']) +const { t } = useI18n() +const router = useRouter() +const props = defineProps({ + data: { + type: Object as PropType<DataCoreType>, + default: {}, + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType>, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowInfoType>, + default: {}, + required: true, + }, + authinfo: { + type: Object as PropType<AuthInfoType>, + default: { editable: false, viewchange: false }, + required: true, + }, +}) +const assets_path = useState().ASSETS_PATH.value +const showinfo = ref(props.showinfo as ShowInfoType) +const authinfo = computed(() => { + return showinfo.value.auth ? showinfo.value.auth : props.authinfo +}) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value.corelang && props.localedata.value.corelang[itm] ? props.localedata.value.corelang[itm] : props.data[itm] +} +const onView = (itm: string) => { + if (typeof showinfo.value[itm] !== 'undefined') { + showinfo.value[itm] = !showinfo.value[itm] + if (itm === 'skills') + useState().showinfo.value.skills = showinfo.value[itm] + } +} +const onEditor = (info: { src: string, field: string, idx: number, data: string, ev: Event }) => { + let has_changed = false + let arr_data: string[] = [] + let data = '' + const arr_src = info.field.split('.') + switch (arr_src[0]) { + case 'mission_how': + const field = arr_src.length > 0 ? arr_src[1] : info.field + arr_data = info.data.replace('<ul>', '').replace('</ul>', '').replace(/<li.+?>/g, '').split('</li>') + data = get_data(arr_src[0])[info.idx][field] + break + default: + has_changed = info.data.replace('<p>', '').replace('</p>', '') !== get_data(info.field) + } + if (arr_data.length > 0) { + arr_data = arr_data.filter(it => it !== '') + arr_data.forEach((it, idx) => { + if (!has_changed && it.replace('<p>', '').replace('</p>', '') !== data ) { + has_changed = true + } + }) + } + if (has_changed) { + emit('onEditor', { ...info, arr_data }) + } +} +// const onEditor = (info: { src: string, idx: number, data: string, ev: Event }) => { +// emit('onEditor', { ...info }) +// } +const onItem = (src: string, idx: number) => { + emit('onItem', { root: 'info', src, itm: props.data[src][idx], idx }) +} +const onSkills = (data: { src: string, idx: number }) => { + emit('onItem', { root: 'info', src: data.src, itm: props.data[data.src][data.idx], idx: data.idx }) +} +const onLink = (e: any) => { + track_action(e) +} +</script> \ No newline at end of file diff --git a/src/views/cv/Others.vue b/src/views/cv/Others.vue new file mode 100644 index 0000000..7b04e64 --- /dev/null +++ b/src/views/cv/Others.vue @@ -0,0 +1,80 @@ +<template> + <ul class="list-circle ml-5"> + <li v-for="others,index in get_data('others')" :key="index" :class="{'no-list': !others.auth.show}"> + <div class="other" v-if="others.auth.show" :id="`other-${index}`"> + <tiptap-editor + v-if="authinfo.editable" + :data="others.desc" + :editable="authinfo.editable" + src="others" + field="desc" + :idx="index" + :htmlattrs="htmlattrs" + @onEditorBlur="onEditor" + /> + <div v-else v-html="others.desc.replace('<ul>','<ul class=\'list-circle ml-5\'>')"></div> + </div> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400 -mt-4" + @click.prevent="onItem(index)" + > + <div v-if="others.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through">{{ index }}</div> + </div> + </button> + </div> + </li> + </ul> +</template> +<script setup lang="ts"> +/* +<tiptap-editor +:data="data.others.join('')" +:editable="authinfo.editable" +src="others" +@onEditorBlur="onEditor" +/> +*/ +import { PropType } from 'vue' +import { OtherType, ShowInfoType, DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +const props = defineProps({ + data: { + type: Array as PropType<OtherType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowInfoType>, + default: {}, + required: true, + }, +}) +const emit = defineEmits(['onEditor', 'onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value[itm] ? props.localedata.value[itm] : props.data +} +const htmlattrs = { ...useState().htmlAttrs, bold: 'itm-title' } +const authinfo = useState().authinfo.value +const onEditor = (info: { src: string, idx: number, data: string, ev: Event }) => { + if (info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx].desc) + emit('onEditor', { ...info }) +} +const onItem = (idx: number) => { + emit('onItem', { src: 'others', itm: props.data[idx], idx }) +} +</script> \ No newline at end of file diff --git a/src/views/cv/Profile.vue b/src/views/cv/Profile.vue new file mode 100644 index 0000000..504e599 --- /dev/null +++ b/src/views/cv/Profile.vue @@ -0,0 +1,74 @@ +<template> + <section v-for="profile,index in get_data('profile')" :key="index" class="ml-5"> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="profile.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through text-xs">{{ index }}</div> + </div> + </button> + </div> + <div class="other" v-if="profile.auth.show" :id="`profile-${index}`"> + <tiptap-editor + v-if="authinfo.editable" + :data="profile.desc" + :editable="authinfo.editable" + src="info.profile" + field="desc" + :idx="index" + @onEditorBlur="onEditor" + /> + <div v-else v-html="profile.desc" /> + </div> + </section> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { ProfileType, AuthInfoType, ShowInfoType,DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +const emit = defineEmits(['onEditor', 'onItem']) +const props = defineProps({ + data: { + type: Array as PropType<ProfileType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType>, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowInfoType>, + default: {}, + required: true, + }, + authinfo: { + type: Object as PropType<AuthInfoType>, + default: { editable: false, viewchange: false }, + required: true, + }, +}) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value.corelang && props.localedata.value.corelang[itm] ? props.localedata.value.corelang[itm] : props.data +} +const authinfo = computed(() => { + return props.showinfo.auth ? props.showinfo.auth : props.authinfo +}) +const onEditor = (info: { src: string, idx: number, data: string, ev: Event }) => { + if (info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx].desc) + emit('onEditor', { ...info }) +} +const onItem = (idx: number) => { + emit('onItem', { root: 'info', src: 'profile', itm: props.data[idx], idx }) +} +</script> \ No newline at end of file diff --git a/src/views/cv/Projects.vue b/src/views/cv/Projects.vue new file mode 100644 index 0000000..9dc8cc7 --- /dev/null +++ b/src/views/cv/Projects.vue @@ -0,0 +1,214 @@ +<template> + <div v-for="(item: any,index: number) in get_data('projects')" :key="index" class="project-item mt-2"> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="item.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through text-xs">{{ index }}</div> + </div> + </button> + </div> + <div v-if="item.auth.show" :id="`project-${index}`"> + <div v-for="(info:any,key:any,infoindex: number) in showinfo" :key="infoindex"> + <section + v-if="info && item[key] && key !== 'img' && key !== 'features' && key !== 'builtwith' && key !== 'auth'" + :class="key === 'purpose' ? 'mt-2' : 'mb-0'" + > + <div class="left-item"> + <div v-if="key === 'name'" class="project-name text-center"> + <tiptap-editor + v-if="authinfo.editable" + :data="item.name" + :editable="authinfo.editable" + src="projects" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <div v-else v-html="item.name"></div> + </div> + <span v-else class="text-gray-400 dark:text-gray-500">{{ t(key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <span v-if="key === 'name' && item.img && item.img.length > 0"> + <span v-if="item.site.includes && item.site.includes('http')" class="link"> + <a @click="onLink" :href="item.site" rel="noopener noreferrer" target="_blank"> + <img :src="`${assets_path}${item.img}`" :alt="item.name" class="project mt-2" /> + </a> + </span> + <img v-else :src="`${assets_path}${item.img}`" :alt="item.name" class="project mt-2" /> + </span> + <span v-else> + <span v-if="item[key].includes && item[key].includes('http')" class="link"> + <a + :href="item[key]" + rel="noopener noreferrer" + @click="onLink" + target="_blank" + >{{ item[key].split('://')[1] }}</a> + </span> + <span v-else> + <div v-if="key === 'description' || key === 'purpose'" :class="`project-${key}`"> + <ul class="list-circle"> + <tiptap-editor + v-if="authinfo.editable" + :data="item[key]" + :editable="authinfo.editable" + src="projects" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="item[key]" /> + </ul> + </div> + <div v-else> + <tiptap-editor + v-if="authinfo.editable" + :data="item[key]" + :editable="authinfo.editable" + src="projects" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="item[key]" /> + </div> + </span> + </span> + </div> + </section> + <section v-if="key === 'features' && item.features && Object.keys(item.features).length > 0"> + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key, key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item features"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<li>${item[key].join('</li><li>')}</li>`" + :editable="authinfo.editable" + src="projects" + :field="key" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'itm-title font-normal' }" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="`<li>${item[key].join('</li><li>')}</li>`" /> + </div> + </section> + <section v-if="key === 'builtwith' && item.builtwith && Object.keys(item.builtwith).length > 0"> + <span + class="text-gray-400 dark:text-gray-500" + >{{ t('builtwith', 'Builtwith').toLocaleLowerCase() }}</span> + <div class="tag-list my-3"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<b>${item[key].join('</b> <b>')}</b> `" + :editable="authinfo.editable" + src="projects" + :field="key" + :idx="index" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'tag-item font-light mb-2' }" + @onEditorBlur="onEditor" + /> + <div v-else> + <span + v-for="(built: string,builtindex: number) in item[key]" + :key="builtindex" + class="tag-item font-light mb-2" + >{{ built }}</span> + </div> + </div> + </section> + </div> + </div> + <hr v-if="item.auth.show && index < data.length - 1" class="hr-sep-itms" /> + </div> +</template> +<script setup lang="ts"> +// <span :class="`project-${key}`" v-if="key === 'description' || key === 'purpose'"> +import { PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import { ProjectType, ShowProjectType, DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +import { track_action } from '~/hooks/tracking' +// import { scryptSync } from 'crypto' +const { t } = useI18n() +const router = useRouter() +const props = defineProps({ + data: { + type: Array as PropType<ProjectType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowProjectType>, + default: {}, + required: true, + }, + // authinfo: { + // type: Object as PropType<AuthInfoType>, + // default: { editable: false, viewchange: false }, + // required: true, + // }, +}) +const emit = defineEmits(['onEditor', 'onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value[itm] ? props.localedata.value[itm] : props.data +} +const assets_path = useState().ASSETS_PATH +const authinfo = useState().authinfo.value +// const authinfo = computed(() => { +// return props.showinfo.auth ? props.showinfo.auth : props.authinfo +// }) +const onEditor = (info: { src: string, field: string, idx: number, data: string, ev: Event }) => { + let has_changed = false + let arr_data: string[] = [] + switch (info.field) { + case 'features': + arr_data = info.data.replace('<ul>', '').replace('</ul>', '').replace(/<li.+?>/g, '').split('</li>') + break + case 'builtwith': + arr_data = info.data.replace(/<strong.+?>/g, '').replace('<p>', '').replace('</p>', '').split('</strong>') + break + default: + has_changed = info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field] + } + if (arr_data.length > 0) { + arr_data = arr_data.filter(it => it !== '') + arr_data.forEach((it, idx) => { + if (it.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field][idx] && !has_changed) { + has_changed = true + } + }) + } + if (has_changed) { + emit('onEditor', { ...info, arr_data }) + } +} +const onItem = (idx: number) => { + emit('onItem', { src: 'projects', itm: props.data[idx], idx }) +// const el = e.target && e.target.closest ? e.target.closest('.project-item') : null +// if (el) +// el.style.display = 'none' +} +const onLink = (e: any) => { + track_action(e) +} + +</script> \ No newline at end of file diff --git a/src/views/cv/Skills.vue b/src/views/cv/Skills.vue new file mode 100644 index 0000000..82b0f59 --- /dev/null +++ b/src/views/cv/Skills.vue @@ -0,0 +1,71 @@ +<template> + <ul class="list-none"> + <li v-for="(skill,index) in get_data('skills')" :key="skill.id" :id="`cv-${skill.id}`" class="flex"> + <div class="flex-grow"> + <label :for="skill.id" class="flex flex-row"> + <span v-if="skill.auth.show" class="flex-grow text-base">{{ skill.title }}</span> + <span v-if="skill.auth.show" class="mt-1 text-sm flex-grow-0">{{ skill.value }}%</span> + </label> + <progress + v-if="skill.auth.show" + class="noprint" + :id="skill.id" + :max="skill.max" + :value="skill.value" + ></progress> + </div> + <div class="flex flex-grow-0 ml-2"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="skill.auth.show" i-carbon-view-off /> + <div v-else class="noprint flex"> + <div class="-mt-0.5 mr-2 line-through">{{ index }}</div> + <div i-carbon-view /> + </div> + </button> + </div> + </li> + </ul> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { AuthInfoType, SkillsType, DataLangsType } from '~/typs/cv' +const props = defineProps({ + data: { + type: Array as PropType<SkillsType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + authinfo: { + type: Object as PropType<AuthInfoType>, + default: { editable: false, viewchange: false }, + required: true, + }, +}) +const emit = defineEmits(['onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value.corelang && props.localedata.value.corelang[itm] ? props.localedata.value.corelang[itm] : props.data +} +const onItem = (idx: number) => { + emit('onItem', { src: 'skills', itm: props.data[idx], idx }) +} +// const onItem = (itm: string) => { +// const dom_id = document.getElementById(`cv-${itm}`) +// if (dom_id) { +// dom_id.style.display = 'none' +// } +//} +// onBeforeMount(async() => { +// const p = props +// debugger +// }) +</script> \ No newline at end of file diff --git a/src/views/cv/Talks.vue b/src/views/cv/Talks.vue new file mode 100644 index 0000000..75ee9f2 --- /dev/null +++ b/src/views/cv/Talks.vue @@ -0,0 +1,135 @@ +<template> + <div v-for="(item,index) in get_data('talks')" :key="index" class="talk-item"> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="item.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through text-xs">{{ index }}</div> + </div> + </button> + </div> + <div v-if="item.auth.show" :id="`talk-${index}`"> + <div v-for="info,key,infoindex in showinfo" :key="infoindex" class> + <section v-if="info && item[key] && key !== 'description' && key !== 'auth'" class="flex mb-0"> + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <span v-if="item[key].includes && item[key].includes('http')" class="link"> + <a + :href="item[key]" + rel="noopener noreferrer" + target="_blank" + >{{ item[key].split('://')[1] }}</a> + </span> + <span v-else> + <tiptap-editor + v-if="authinfo.editable" + :data="item[key]" + :editable="authinfo.editable" + src="talks" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else>{{ item[key] }}</span> + </span> + </div> + </section> + <section + v-if="key === 'description' && item.description && Object.keys(item.description).length > 0" + class="mt-0" + > + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key, key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <ul class="list-circle"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<li>${item[key].join('</li><li>')}</li>`" + :editable="authinfo.editable" + src="talks" + :field="key" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'itm-title font-normal' }" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="`<li>${item[key].join('</li><li>')}</li>`" /> + </ul> + </div> + </section> + </div> + </div> + <hr v-if="item.auth.show && index < data.length - 1" class="hr-sep-itms" /> + </div> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import { TalksType, ShowTalksType, DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +const { t } = useI18n() +const props = defineProps({ + data: { + type: Array as PropType<TalksType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowTalksType>, + default: {}, + required: true, + }, +}) +const emit = defineEmits(['onEditor', 'onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value[itm] ? props.localedata.value[itm] : props.data +} +const authinfo = useState().authinfo.value +const onEditor = (info: { src: string, field: string, idx: number, data: string, ev: Event }) => { + let has_changed = false + let arr_data: string[] = [] + switch (info.field) { + case 'description': + arr_data = info.data.replace('<ul>', '').replace('</ul>', '').replace(/<li.+?>/g, '').split('</li>') + break + default: + has_changed = info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field] + } + if (arr_data.length > 0) { + arr_data = arr_data.filter(it => it !== '') + arr_data.forEach((it, idx) => { + if (it.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field][idx] && !has_changed) { + has_changed = true + } + }) + } + if (has_changed) { + emit('onEditor', { ...info, arr_data }) + } +} +const onItem = (idx: number) => { + emit('onItem', { src: 'talks', itm: props.data[idx], idx }) +} +// const onItem = (e: any) => { +// const el = e.target && e.target.closest ? e.target.closest('.talk-item') : null +// if (el) +// el.style.display='none' +//} +</script> \ No newline at end of file diff --git a/src/views/cv/Teaching.vue b/src/views/cv/Teaching.vue new file mode 100644 index 0000000..e16e5b0 --- /dev/null +++ b/src/views/cv/Teaching.vue @@ -0,0 +1,135 @@ +<template> + <div v-for="(item,index) in get_data('teaching')" :key="index" :id="`teaching-${index}`" class="teaching-item"> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="item.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through text-xs">{{ index }}</div> + </div> + </button> + </div> + <div v-if="item.auth.show" :id="`teaching-${index}`"> + <div v-for="info,key,infoindex in showinfo" :key="infoindex" class> + <section v-if="info && item[key] && key !== 'description' && key !== 'auth'" class="flex mb-0"> + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <span v-if="item[key].includes && item[key].includes('http')" class="link"> + <a + :href="item[key]" + rel="noopener noreferrer" + target="_blank" + >{{ item[key].split('://')[1] }}</a> + </span> + <span v-else> + <tiptap-editor + v-if="authinfo.editable" + :data="item[key]" + :editable="authinfo.editable" + src="teaching" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else>{{ item[key] }}</span> + </span> + </div> + </section> + <section + v-if="key === 'description' && item.description && Object.keys(item.description).length > 0" + class="mt-0" + > + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key, key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <ul class="list-circle"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<li>${item[key].join('</li><li>')}</li>`" + :editable="authinfo.editable" + src="teaching" + :field="key" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'itm-title font-normal' }" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="`<li>${item[key].join('</li><li>')}</li>`" /> + </ul> + </div> + </section> + </div> + </div> + <hr v-if="item.auth.show && index < data.length - 1" class="hr-sep-itms" /> + </div> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import { TeachingType, ShowTeachingType, DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +const { t } = useI18n() +const props = defineProps({ + data: { + type: Array as PropType<TeachingType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowTeachingType>, + default: {}, + required: true, + }, +}) +const emit = defineEmits(['onEditor', 'onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value[itm] ? props.localedata.value[itm] : props.data +} +const authinfo = useState().authinfo.value +const onEditor = (info: { src: string, field: string, idx: number, data: string, ev: Event }) => { + let has_changed = false + let arr_data: string[] = [] + switch (info.field) { + case 'description': + arr_data = info.data.replace('<ul>', '').replace('</ul>', '').replace(/<li.+?>/g, '').split('</li>') + break + default: + has_changed = info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field] + } + if (arr_data.length > 0) { + arr_data = arr_data.filter(it => it !== '') + arr_data.forEach((it, idx) => { + if (it.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field][idx] && !has_changed) { + has_changed = true + } + }) + } + if (has_changed) { + emit('onEditor', { ...info, arr_data }) + } +} +const onItem = (idx: number) => { + emit('onItem', { src: 'teaching', itm: props.data[idx], idx }) +} +// const onItem = (e: any) => { +// const el = e.target && e.target.closest ? e.target.closest('.teaching-item') : null +// if (el) +// el.style.display = 'none' +// } +</script> \ No newline at end of file diff --git a/src/views/cv/WorkExperience.vue b/src/views/cv/WorkExperience.vue new file mode 100644 index 0000000..8e36d5f --- /dev/null +++ b/src/views/cv/WorkExperience.vue @@ -0,0 +1,183 @@ +<template> + <div v-for="(item,index) in get_data('work_experiences')" :key="index" class="experience-item"> + <div class="flex"> + <span class="w-4/5 flex-grow" /> + <button + v-if="authinfo.viewchange" + class="noprint flex-grow-0 icon-btn !outline-none text-gray-500 dark:text-gray-400" + @click.prevent="onItem(index)" + > + <div v-if="item.auth.show" class="noprint flex text-gray-400 dark:text-gray-500"> + <div i-carbon-view-off /> + <div class="mt-0.2 ml-2">{{ index }}</div> + </div> + <div v-else class="noprint flex text-indigo-400 dark:text-indigo-500"> + <div i-carbon-view /> + <div class="mt-0.2 ml-2 line-through">{{ index }}</div> + </div> + </button> + </div> + <div v-if="item.auth.show" :id="`experience-${index}`"> + <div v-for="info,key,infoindex in showinfo" :key="infoindex"> + <section + v-if="info && item[key] && key !== 'wheredef' && key !== 'tools' && key !== 'tasks' && key !== 'auth'" + class="mb-0" + > + <div class="left-item"> + <h3 v-if="key === 'where'"> + <tiptap-editor + v-if="authinfo.editable" + :data="item.where" + :editable="authinfo.editable" + src="work_experience" + field="where" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="item.where"></span> + </h3> + <span + class="text-gray-400 dark:text-gray-500" + v-if="key !== 'where' && key !== 'wheredef'" + >{{ t(key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <span v-if="key === 'where'"> + <tiptap-editor + v-if="authinfo.editable" + :data="item.wheredef" + :editable="authinfo.editable" + src="work_experience" + field="wheredef" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else>{{ item.wheredef }}</span> + </span> + <span v-if="key !== 'wheredef' && key !== 'where'"> + <tiptap-editor + v-if="authinfo.editable" + :data="item[key]" + :editable="authinfo.editable" + src="work_experience" + :field="key" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else>{{ item[key] }}</span> + </span> + </div> + </section> + <section + v-if="key === 'tasks' && item.tasks && Object.keys(item.tasks).length > 0" + class="mt-1" + > + <div class="left-item"> + <span class="text-gray-400 dark:text-gray-500">{{ t(key, key).toLocaleLowerCase() }}</span> + </div> + <div class="right-item"> + <ul class="list-circle"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<li>${item[key].join('</li><li>')}</li>`" + :editable="authinfo.editable" + src="work_experience" + :field="key" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'itm-title font-normal' }" + :idx="index" + @onEditorBlur="onEditor" + /> + <span v-else v-html="`<li>${item[key].join('</li><li>')}</li>`" /> + </ul> + </div> + </section> + <section v-if="key === 'tools' && item.tools && Object.keys(item.tools).length > 0" class> + <span class="text-gray-400 dark:text-gray-500">{{ t('tools', 'Tools').toLocaleLowerCase() }}</span> + <div class="tag-list my-3"> + <tiptap-editor + v-if="authinfo.editable" + :data="`<b>${item[key].join('</b> <b>')}</b> `" + :editable="authinfo.editable" + src="work_experience" + :field="key" + :idx="index" + :htmlattrs="{ ...useState().htmlAttrs, bold: 'tag-item font-light mb-2 leading-loose' }" + @onEditorBlur="onEditor" + /> + <span + v-else + v-for="tool,toolindex in item[key]" + :key="toolindex" + class="tag-item font-light mb-2" + >{{ tool }}</span> + </div> + </section> + </div> + </div> + <hr v-if="item.auth.show && index < data.length - 1" class="hr-sep-itms" /> + </div> +</template> +<script setup lang="ts"> +import { PropType } from 'vue' +import { useI18n } from 'vue-i18n' +import { WorkExperienceType, ShowWorkExperienceType, DataLangsType } from '~/typs/cv' +import TiptapEditor from '~/components/TiptapEditor.vue' +import useState from '~/hooks/useState' +const { t } = useI18n() +const props = defineProps({ + data: { + type: Array as PropType<WorkExperienceType[]>, + default: [], + required: true, + }, + localedata: { + type: Object as PropType<DataLangsType> | null, + default: {}, + required: true, + }, + showinfo: { + type: Object as PropType<ShowWorkExperienceType>, + default: {}, + required: true, + }, +}) +const emit = defineEmits(['onEditor', 'onItem']) +const get_data = (itm: string) => { + return props.localedata.value && props.localedata.value[itm] ? props.localedata.value[itm] : props.data +} +const authinfo = useState().authinfo.value +const onEditor = (info: { src: string, field: string, idx: number, data: string, ev: Event }) => { + let has_changed = false + let arr_data: string[] = [] + switch (info.field) { + case 'tasks': + arr_data = info.data.replace('<ul>', '').replace('</ul>', '').replace(/<li.+?>/g, '').split('</li>') + break + case 'tools': + arr_data = info.data.replace(/<strong.+?>/g, '').replace('<p>', '').replace('</p>', '').split('</strong>') + break + default: + has_changed = info.data.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field] + } + if (arr_data.length > 0) { + arr_data = arr_data.filter(it => it !== '') + arr_data.forEach((it, idx) => { + if (it.replace('<p>', '').replace('</p>', '') !== props.data[info.idx][info.field][idx] && !has_changed) { + has_changed = true + } + }) + } + if (has_changed) { + emit('onEditor', { ...info, arr_data }) + } +} +const onItem = (idx: number) => { + emit('onItem', { src: 'work_experience', itm: props.data[idx], idx }) +} +// const onItem = (e: any) => { +// const el = e.target && e.target.closest ? e.target.closest('.experience-item') : null +// if (el) { +// el.style.display = el.style.display === 'none' ? '' : 'none' +// } +// } +</script> \ No newline at end of file