chore: add code'
This commit is contained in:
		
							parent
							
								
									326518cbf9
								
							
						
					
					
						commit
						21d2ccb462
					
				
							
								
								
									
										21
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/App.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/components/Footer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/Footer.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										92
									
								
								src/components/MenuLocales.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										92
									
								
								src/components/MenuLocales.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										41
									
								
								src/components/MessageBox.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								src/components/MessageBox.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										191
									
								
								src/components/MessageBoxView.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										191
									
								
								src/components/MessageBoxView.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										166
									
								
								src/components/Modal.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								src/components/Modal.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										307
									
								
								src/components/NavMenu.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										307
									
								
								src/components/NavMenu.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										76
									
								
								src/components/Navbar.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								src/components/Navbar.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/components/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/components/README.md
									
									
									
									
									
										Normal file
									
								
							@ -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.
 | 
				
			||||||
							
								
								
									
										212
									
								
								src/components/TiptapEditor.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								src/components/TiptapEditor.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										2
									
								
								src/composables/dark.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								src/composables/dark.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,2 @@
 | 
				
			|||||||
 | 
					export const isDark = useDark()
 | 
				
			||||||
 | 
					export const toggleDark = useToggle(isDark)
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/composables/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/composables/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './dark'
 | 
				
			||||||
							
								
								
									
										110
									
								
								src/hooks/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										110
									
								
								src/hooks/config.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										112
									
								
								src/hooks/loads.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										112
									
								
								src/hooks/loads.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										48
									
								
								src/hooks/tracking.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								src/hooks/tracking.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										97
									
								
								src/hooks/useComponent.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/hooks/useComponent.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										189
									
								
								src/hooks/useState.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										189
									
								
								src/hooks/useState.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										178
									
								
								src/hooks/utils.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								src/hooks/utils.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										270
									
								
								src/hooks/utilsAuth.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										270
									
								
								src/hooks/utilsAuth.ts
									
									
									
									
									
										Normal file
									
								
							@ -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,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/layouts/Default.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/layouts/Default.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										8
									
								
								src/layouts/SimpleLayout.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/layouts/SimpleLayout.vue
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,8 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div class="antialiased text-gray-900 bg-white">
 | 
				
			||||||
 | 
					    <slot />
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					  <Footer />
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<script setup></script>
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/logics/dark.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/logics/dark.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 },
 | 
				
			||||||
 | 
					)
 | 
				
			||||||
							
								
								
									
										1
									
								
								src/logics/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/logics/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					export * from './dark'
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/logics/store.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/logics/store.ts
									
									
									
									
									
										Normal file
									
								
							@ -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'>
 | 
				
			||||||
							
								
								
									
										35
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/main.ts
									
									
									
									
									
										Normal file
									
								
							@ -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')
 | 
				
			||||||
							
								
								
									
										11
									
								
								src/modules/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/modules/README.md
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/modules/i18n.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/modules/i18n.ts
									
									
									
									
									
										Normal file
									
								
							@ -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)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					*/
 | 
				
			||||||
							
								
								
									
										9
									
								
								src/modules/nprogress.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/modules/nprogress.ts
									
									
									
									
									
										Normal file
									
								
							@ -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() })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										12
									
								
								src/modules/sw.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/modules/sw.ts
									
									
									
									
									
										Normal file
									
								
							@ -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 })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								src/pages/About.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								src/pages/About.md
									
									
									
									
									
										Normal file
									
								
							@ -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.
 | 
				
			||||||
							
								
								
									
										20
									
								
								src/pages/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/pages/README.md
									
									
									
									
									
										Normal file
									
								
							@ -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'
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
							
								
								
									
										5
									
								
								src/pages/[...all].vue
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								src/pages/[...all].vue
									
									
									
									
									
										Executable file
									
								
							@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<template>
 | 
				
			||||||
 | 
					  <div>
 | 
				
			||||||
 | 
					    Not Found
 | 
				
			||||||
 | 
					  </div>
 | 
				
			||||||
 | 
					</template>
 | 
				
			||||||
							
								
								
									
										50
									
								
								src/pages/base.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								src/pages/base.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										25
									
								
								src/pages/hi/[name].vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								src/pages/hi/[name].vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										56
									
								
								src/router.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								src/router.ts
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
							
								
								
									
										60
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								src/shims.d.ts
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										254
									
								
								src/styles/main.css
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										254
									
								
								src/styles/main.css
									
									
									
									
									
										Executable file
									
								
							@ -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; }
 | 
				
			||||||
 | 
					} */
 | 
				
			||||||
							
								
								
									
										86
									
								
								src/typs/clouds/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								src/typs/clouds/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										59
									
								
								src/typs/cmpnts/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										59
									
								
								src/typs/cmpnts/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
							
								
								
									
										4
									
								
								src/typs/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/typs/config.ts
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,4 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
 | 
					export interface ConfUrlsType {
 | 
				
			||||||
 | 
						[key: string]: string
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										320
									
								
								src/typs/cv.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										320
									
								
								src/typs/cv.ts
									
									
									
									
									
										Normal file
									
								
							@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/typs/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/typs/index.ts
									
									
									
									
									
										Normal file
									
								
							@ -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',
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										28
									
								
								src/views/404.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										28
									
								
								src/views/404.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										584
									
								
								src/views/Home.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										584
									
								
								src/views/Home.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										165
									
								
								src/views/cv/Education.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								src/views/cv/Education.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										476
									
								
								src/views/cv/InfoPanel.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										476
									
								
								src/views/cv/InfoPanel.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										80
									
								
								src/views/cv/Others.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								src/views/cv/Others.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										74
									
								
								src/views/cv/Profile.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										74
									
								
								src/views/cv/Profile.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										214
									
								
								src/views/cv/Projects.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										214
									
								
								src/views/cv/Projects.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										71
									
								
								src/views/cv/Skills.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								src/views/cv/Skills.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										135
									
								
								src/views/cv/Talks.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/views/cv/Talks.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										135
									
								
								src/views/cv/Teaching.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								src/views/cv/Teaching.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
							
								
								
									
										183
									
								
								src/views/cv/WorkExperience.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										183
									
								
								src/views/cv/WorkExperience.vue
									
									
									
									
									
										Normal file
									
								
							@ -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>
 | 
				
			||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user