minhnt-dev: make UI
This commit is contained in:
@@ -0,0 +1,22 @@
|
|||||||
|
.style_layout {
|
||||||
|
> .section-container {
|
||||||
|
> .layout_define {
|
||||||
|
> .section_layout {
|
||||||
|
@apply gap-x-14
|
||||||
|
|
||||||
|
> div {
|
||||||
|
background: red;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
article {
|
||||||
|
background: red;
|
||||||
|
h3 {
|
||||||
|
@apply text-xl #{!important}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-11
@@ -37,7 +37,7 @@ const _dataResult = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="collection-container p-2" :class="LAYOUT_PARSE['LAYOUT'] || 'horizontal'">
|
<div class="collection-container grid gap-5" :class="LAYOUT_PARSE['LAYOUT'] || 'horizontal'">
|
||||||
<div v-for="(component, index) in _dataResult" :key="index">
|
<div v-for="(component, index) in _dataResult" :key="index">
|
||||||
<template v-if="!isEmpty(component)">
|
<template v-if="!isEmpty(component)">
|
||||||
<DynamicComponent
|
<DynamicComponent
|
||||||
@@ -65,8 +65,6 @@ const _dataResult = computed(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.collection-container {
|
.collection-container {
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -74,13 +72,5 @@ const _dataResult = computed(() => {
|
|||||||
grid-template-rows: auto;
|
grid-template-rows: auto;
|
||||||
grid-auto-flow: column;
|
grid-auto-flow: column;
|
||||||
}
|
}
|
||||||
.empty {
|
|
||||||
min-height: 100px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #409eff;
|
|
||||||
}
|
|
||||||
&.noData {
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -46,26 +46,30 @@ const drop = (e: any) => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<article class="basic-article" :class="[LAYOUT_PARSE['LAYOUT'] || 'horizontal', !parseData && 'no-data', LAYOUT_PARSE['REVERSE'] ? 'reverse' : '']" @click="selectComponent" @dragover.prevent @drop.stop.prevent="drop">
|
<article class="basic-article gap-x-4" :class="[LAYOUT_PARSE['LAYOUT'] || 'horizontal', !parseData && 'no-data', LAYOUT_PARSE['REVERSE'] ? 'reverse' : '']">
|
||||||
<div v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('thumbnail')" class="basic-article_thumbnail">
|
<div v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('thumbnail')" class="basic-article_thumbnail">
|
||||||
<template v-if="parseData">
|
<template v-if="parseData">
|
||||||
<img class="object-fit-cover" :src="parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'" :alt="parseData.title?.replace(/<[^>]+>/g, '')" />
|
<img class="object-fit-cover" :src="parseData.thumbnail ? parseData.thumbnail : '/images/default-thumbnail.jpg'" :alt="parseData.title?.replace(/<[^>]+>/g, '')" />
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="empty-block" style="width: 100%; height: 100%; min-height: 50px;"></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="basic-article_content" :class="[!parseData && 'no-data']">
|
<div class="basic-article_content" :class="[!parseData && 'no-data']">
|
||||||
<div>
|
<div>
|
||||||
<h3 v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('title')" class="mb-1 text-truncate-two-lines">
|
<template v-if="parseData">
|
||||||
|
<nuxt-link :to="`/bai-viet/${parseData.slug}`">
|
||||||
|
<h3 v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('title')" class="mb-1 line-clamp-2 text-base font-600 hover:text-primary-100 transition-all duration-300">
|
||||||
|
{{ parseData.title?.replace(/<[^>]+>/g, '') }}
|
||||||
|
</h3>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<p v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('paragraph')" class="mb-0 line-clamp-2">
|
||||||
<template v-if="parseData">
|
<template v-if="parseData">
|
||||||
{{ parseData.title?.replace(/<[^>]+>/g, '') }}
|
<template v-if="parseData.intro">
|
||||||
|
{{ parseData.intro?.replace(/<[^>]+>/g, '') }}
|
||||||
|
</template>
|
||||||
|
<template v-if="parseData.sub">
|
||||||
|
{{ parseData.sub?.replace(/<[^>]+>/g, '') }}
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<span v-else class="empty-block" style="height: 8px;"></span>
|
|
||||||
</h3>
|
|
||||||
<p v-if="!LAYOUT_PARSE['HIDE'] || !LAYOUT_PARSE['HIDE'].includes('paragraph')" class="mb-0 text-truncate-two-lines">
|
|
||||||
<template v-if="parseData">
|
|
||||||
{{ parseData.intro?.replace(/<[^>]+>/g, '') }}
|
|
||||||
</template>
|
|
||||||
<span v-else class="empty-block" style="height: 5px;"></span>
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -75,19 +79,20 @@ const drop = (e: any) => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.basic-article {
|
.basic-article {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
&.no-data {
|
|
||||||
gap: 5px !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.vertical {
|
&.vertical {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
|
.basic-article_content {
|
||||||
|
padding: 10px 0px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.horizontal {
|
&.horizontal {
|
||||||
grid-template-columns: 1fr 1fr;
|
grid-template-columns: 1fr 1fr;
|
||||||
|
.basic-article_content {
|
||||||
|
padding: 0px 0px;
|
||||||
|
}
|
||||||
|
|
||||||
&.reverse {
|
&.reverse {
|
||||||
.basic-article_thumbnail {
|
.basic-article_thumbnail {
|
||||||
@@ -110,23 +115,6 @@ const drop = (e: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&_content {
|
|
||||||
padding: 10px 0px;
|
|
||||||
|
|
||||||
&.no-data {
|
|
||||||
padding: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
p {
|
|
||||||
font-size: 12px;
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.empty-block {
|
.empty-block {
|
||||||
background-color: #409eff;
|
background-color: #409eff;
|
||||||
height: 100px;
|
height: 100px;
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { isEmpty } from "lodash";
|
|
||||||
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from '@/utils/parseSQL';
|
|
||||||
|
|
||||||
const emit = defineEmits(["dropData", "selectComponent"]);
|
import { COLLECTION_QUERY_DROP, getValueStringWithKeyAndColon, getInputValue } from '@/utils/parseSQL';
|
||||||
|
|
||||||
const _props = defineProps<{
|
const _props = defineProps<{
|
||||||
dataResult?: any[];
|
dataResult?: any[];
|
||||||
@@ -21,75 +19,18 @@ const _dataResult = computed(() => {
|
|||||||
})
|
})
|
||||||
return _components;
|
return _components;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function dropData(event: any) {
|
|
||||||
const { dataResult, dataType } = JSON.parse(event.dataTransfer.getData("category"));
|
|
||||||
const checkDataResult = getInputValue(_props.dataResult, 'ARRAY');
|
|
||||||
const result = _props.dataResult ? [...checkDataResult, { ...dataResult }] : [{...dataResult}];
|
|
||||||
const getDataQuery = _props.dataQuery ?
|
|
||||||
COLLECTION_QUERY_DROP(dataType, getValueStringWithKeyAndColon(_props.dataQuery) + "," + dataResult.id)
|
|
||||||
: COLLECTION_QUERY_DROP(dataType, dataResult.id);
|
|
||||||
|
|
||||||
emit("dropData", {
|
|
||||||
dataResult: result,
|
|
||||||
dataType,
|
|
||||||
dataQuery: getDataQuery,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const selectComponent = () => {
|
|
||||||
emit("selectComponent");
|
|
||||||
};
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div class="categories-container p-2" @click="selectComponent">
|
<div class="flex gap-4 items-end py-2" @click="selectComponent">
|
||||||
<div
|
<nuxt-link :to="`/${component.code}`" v-for="(component, index) in _dataResult" :key="index" class="font-medium text-[18px] first:font-600 first:text-[24px]">
|
||||||
v-for="(component, index) in _dataResult"
|
<h3 class="m-0 leading-none hover:text-primary-100 transition-all duration-300">{{ component.title }}</h3>
|
||||||
:key="index"
|
</nuxt-link>
|
||||||
:class="isEmpty(component) ? 'empty' : 'category'"
|
|
||||||
>
|
|
||||||
<template v-if="!isEmpty(component)">
|
|
||||||
<h3>{{ component.title }}</h3>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
@dragover.prevent
|
|
||||||
@drop.stop.prevent="dropData($event)"
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.categories-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
align-items: flex-end;
|
|
||||||
.category {
|
|
||||||
height: 100%;
|
|
||||||
h3 {
|
|
||||||
font-weight: 500;
|
|
||||||
font-size: 14px;
|
|
||||||
margin: 0px !important;
|
|
||||||
}
|
|
||||||
&:first-child {
|
|
||||||
h3 {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 17px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.empty {
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #409eff;
|
|
||||||
width: 50px;
|
|
||||||
> div {
|
|
||||||
min-height: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const CLASS_FOR_SECTION = computed(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="section_layout" :class="[CLASS_FOR_SECTION.section_layout]">
|
<div class="section_layout grid gap-x-5 gap-y-2.5" :class="[CLASS_FOR_SECTION.section_layout]">
|
||||||
<div
|
<div
|
||||||
v-for="(position, index) in props.content || Array(SETTING_OPTIONS.MAX_ELEMENT).fill({})"
|
v-for="(position, index) in props.content || Array(SETTING_OPTIONS.MAX_ELEMENT).fill({})"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -154,9 +154,6 @@ const CLASS_FOR_SECTION = computed(() => {
|
|||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.section_layout {
|
.section_layout {
|
||||||
display: grid;
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
&.basic_column {
|
&.basic_column {
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
grid-template-columns: repeat(1, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ const props = defineProps<{
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="section-container">
|
<section class="section-container">
|
||||||
<h2 class="section__title mb-3" v-if="props.label">{{ props.label || '' }}</h2>
|
<h2 class="text-4xl mb-3 font-600" v-if="props.label">{{ props.label || '' }}</h2>
|
||||||
<div class="section_layout">
|
<div class="layout_define grid grid-cols-1 gap-x-10 gap-y-2.5">
|
||||||
<template v-if="props.layout">
|
<template v-if="props.layout">
|
||||||
<DynamicLayout
|
<DynamicLayout
|
||||||
:layout="props.layout"
|
:layout="props.layout"
|
||||||
@@ -24,25 +24,6 @@ const props = defineProps<{
|
|||||||
:section= "props.section"
|
:section= "props.section"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
<template v-else>
|
|
||||||
<div>
|
|
||||||
Bấm để chọn layout
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.section_layout {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
gap: 10px;
|
|
||||||
|
|
||||||
.empty {
|
|
||||||
min-height: 100px;
|
|
||||||
border-radius: 6px;
|
|
||||||
background: #409eff;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -8,20 +8,20 @@ const CLASS_FOR_LAYOUT = computed(() => {
|
|||||||
switch (props.layout) {
|
switch (props.layout) {
|
||||||
case 'Full_Page':
|
case 'Full_Page':
|
||||||
_classForLayout = {
|
_classForLayout = {
|
||||||
page_container: 'page_container full-size-page',
|
page_container: 'page_container w-full',
|
||||||
layout_container: 'layout_container full-size-layout',
|
layout_container: 'layout_container px-5',
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'Center_Page':
|
case 'Center_Page':
|
||||||
_classForLayout = {
|
_classForLayout = {
|
||||||
page_container: 'page_container full-size-page',
|
page_container: 'page_container w-full',
|
||||||
layout_container: 'layout_container center-layout',
|
layout_container: 'layout_container container mx-auto',
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
case 'Background_Page':
|
case 'Background_Page':
|
||||||
_classForLayout = {
|
_classForLayout = {
|
||||||
page_container: 'page_container full-size-page background-container',
|
page_container: 'page_container w-full background-container',
|
||||||
layout_container: 'layout_container center-layout',
|
layout_container: 'layout_container container mx-auto',
|
||||||
};
|
};
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
@@ -37,36 +37,12 @@ const CLASS_FOR_LAYOUT = computed(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="[CLASS_FOR_LAYOUT.page_container]">
|
<div :class="[CLASS_FOR_LAYOUT.page_container]">
|
||||||
<div :class="[CLASS_FOR_LAYOUT.layout_container]" class="grid-container">
|
<div :class="[CLASS_FOR_LAYOUT.layout_container]" class="grid-container grid grid-cols-1 gap-20 style_layout">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.page_container {
|
|
||||||
&.full-size-page {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
.full-size-layout {
|
|
||||||
padding-left: 20px;
|
|
||||||
padding-right: 20px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.layout_container {
|
|
||||||
padding-top: 40px;
|
|
||||||
padding-bottom: 40px;
|
|
||||||
|
|
||||||
&.center-layout {
|
|
||||||
max-width: 1300px;
|
|
||||||
margin: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-container {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(1, minmax(0, 1fr));
|
|
||||||
gap: 40px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
import { clsx, ClassValue } from "clsx";
|
||||||
|
export default (...inputs: ClassValue[]) => twMerge(clsx(inputs));
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import useStorage from '~/composables/useStorage'
|
||||||
|
|
||||||
|
type ArticleReading = {
|
||||||
|
articleId: number
|
||||||
|
readingDuration: number
|
||||||
|
}
|
||||||
|
|
||||||
|
type UserReadingHabits = {
|
||||||
|
accessSpecificSections: number[]
|
||||||
|
accessSpecificArticles: number[]
|
||||||
|
articleReadingDuration:ArticleReading[]
|
||||||
|
readingDuration:number
|
||||||
|
readAndParticipateInDiscussions: number[]
|
||||||
|
useSearchTools: string[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export default (entity?: any) => {
|
||||||
|
const { get, set } = useStorage()
|
||||||
|
|
||||||
|
let startTime = Date.now()
|
||||||
|
|
||||||
|
const getReadingHabits = (): UserReadingHabits => {
|
||||||
|
const readingHabits = get('readingHabits')
|
||||||
|
return readingHabits ? JSON.parse(readingHabits) : {
|
||||||
|
accessSpecificSections: [],
|
||||||
|
accessSpecificArticles: [],
|
||||||
|
articleReadingDuration: [],
|
||||||
|
readingDuration:0,
|
||||||
|
readAndParticipateInDiscussions: [],
|
||||||
|
useSearchTools: []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const setReadingHabits = (readingHabits: UserReadingHabits) => {
|
||||||
|
set('readingHabits', readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addReadingHabit = (readingHabit: ArticleReading) => {
|
||||||
|
const readingHabits = getReadingHabits()
|
||||||
|
const index = readingHabits.articleReadingDuration.findIndex((articleReading) => articleReading.articleId === readingHabit.articleId)
|
||||||
|
if (index !== -1) {
|
||||||
|
readingHabits.articleReadingDuration[index].readingDuration += readingHabit.readingDuration
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
readingHabits.articleReadingDuration.push(readingHabit)
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAccessSpecificSection = (sectionId: number) => {
|
||||||
|
const readingHabits = getReadingHabits()
|
||||||
|
if (readingHabits.accessSpecificSections.includes(sectionId)) return
|
||||||
|
readingHabits.accessSpecificSections.push(sectionId)
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addAccessSpecificArticle = (articleId: number) => {
|
||||||
|
const readingHabits = getReadingHabits()
|
||||||
|
if (readingHabits.accessSpecificArticles.includes(articleId)) return
|
||||||
|
readingHabits.accessSpecificArticles.push(articleId)
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addReadAndParticipateInDiscussions = (discussionId: number) => {
|
||||||
|
const readingHabits = getReadingHabits()
|
||||||
|
if (readingHabits.readAndParticipateInDiscussions.includes(discussionId)) return
|
||||||
|
readingHabits.readAndParticipateInDiscussions.push(discussionId)
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addUseSearchTools = (searchTool: string) => {
|
||||||
|
const readingHabits = getReadingHabits()
|
||||||
|
if (readingHabits.useSearchTools.includes(searchTool)) return
|
||||||
|
readingHabits.useSearchTools.push(searchTool)
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
const addReadingDuration = (readingDuration: number) => {
|
||||||
|
const readingHabits = getReadingHabits()
|
||||||
|
readingHabits.readingDuration += readingDuration
|
||||||
|
setReadingHabits(readingHabits)
|
||||||
|
}
|
||||||
|
|
||||||
|
onBeforeUnmount(()=>{
|
||||||
|
addReadingDuration(Date.now() - startTime)
|
||||||
|
// check if entity is an article, if so add reading duration of article
|
||||||
|
if(entity){
|
||||||
|
addReadingHabit({
|
||||||
|
articleId: entity.id!,
|
||||||
|
readingDuration: Date.now() - startTime
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
getReadingHabits,
|
||||||
|
setReadingHabits,
|
||||||
|
addReadingHabit,
|
||||||
|
addAccessSpecificSection,
|
||||||
|
addAccessSpecificArticle,
|
||||||
|
addReadAndParticipateInDiscussions,
|
||||||
|
addUseSearchTools,
|
||||||
|
addReadingDuration
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
const debounce = (fn: (...args: any[]) => void, delay = 0, immediate = false) => {
|
||||||
|
let timeout: string | number | NodeJS.Timeout
|
||||||
|
return (...args: any[]) => {
|
||||||
|
if (immediate && !timeout) fn(...args)
|
||||||
|
clearTimeout(timeout)
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
fn(...args)
|
||||||
|
}, delay)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const useDebouncedRef = (initialValue: any, delay?: number, immediate?: boolean) => {
|
||||||
|
const state = ref(initialValue)
|
||||||
|
const debouncedRef = customRef((track: () => void, trigger: () => void) => ({
|
||||||
|
get() {
|
||||||
|
track()
|
||||||
|
return state.value
|
||||||
|
},
|
||||||
|
set: debounce(
|
||||||
|
(value: any) => {
|
||||||
|
state.value = value
|
||||||
|
trigger()
|
||||||
|
},
|
||||||
|
delay,
|
||||||
|
immediate
|
||||||
|
),
|
||||||
|
}))
|
||||||
|
return debouncedRef
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useDebouncedRef
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
import { type UseEventBusReturn } from '@vueuse/core'
|
||||||
|
|
||||||
|
type InputProps = {
|
||||||
|
group?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useFormGroup = (inputProps?: InputProps) =>{
|
||||||
|
const profileFormEvent = inject<UseEventBusReturn<any, any>|undefined>('profile-form-event', undefined)
|
||||||
|
const profileFormStatus = inject('profile-form-status', {} as any)
|
||||||
|
|
||||||
|
const status = computed(()=>profileFormStatus?.value[inputProps?.group||''])
|
||||||
|
|
||||||
|
const emitFieldUpdate = (data:any)=>{
|
||||||
|
profileFormEvent?.emit({type:'update',field:inputProps?.group, data})
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeStatus = (status:string)=>{
|
||||||
|
profileFormEvent?.emit({type:'status',field:inputProps?.group, data:status})
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status,
|
||||||
|
emitFieldUpdate,
|
||||||
|
changeStatus
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
export default () => {
|
||||||
|
/**
|
||||||
|
* Retrieves the value associated with the specified key from the local storage.
|
||||||
|
* @param key - The key of the value to retrieve.
|
||||||
|
* @returns The value associated with the specified key, or null if the key does not exist.
|
||||||
|
*/
|
||||||
|
function get(key: string): any {
|
||||||
|
return localStorage.getItem(key) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the value for a given key in the specified storage type.
|
||||||
|
* If the storage type is not provided, it defaults to "local" storage.
|
||||||
|
*
|
||||||
|
* @param key - The key to set the value for.
|
||||||
|
* @param payload - The value to be set.
|
||||||
|
* @param type - The type of storage to use ("local" or "session").
|
||||||
|
*/
|
||||||
|
function set(key: string, payload: any | any[], type: "local" | "session" = "local"): void {
|
||||||
|
// Use sessionStorage if type is "session"
|
||||||
|
const storage = type === "session" ? sessionStorage : localStorage;
|
||||||
|
|
||||||
|
// Convert payload to JSON string if it's an object
|
||||||
|
const data = typeof payload === "object" ? JSON.stringify(payload) : payload;
|
||||||
|
|
||||||
|
// Remove existing item before setting the new one
|
||||||
|
remove(key);
|
||||||
|
|
||||||
|
// Set item in storage
|
||||||
|
storage.setItem(key, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the item with the specified key from the local storage.
|
||||||
|
* @param key - The key of the item to remove.
|
||||||
|
*/
|
||||||
|
function remove(key: string): void {
|
||||||
|
localStorage.removeItem(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
get,
|
||||||
|
set,
|
||||||
|
remove
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<Body class=" text-black max-w-screen max-w-full w-full overflow-x-hidden">
|
<Body class=" text-black max-w-screen max-w-full w-full overflow-x-hidden">
|
||||||
<!-- <Component :is="header[hostname]" /> -->
|
<!-- <Component :is="header[hostname]" /> -->
|
||||||
aaaa
|
|
||||||
<slot />
|
<slot />
|
||||||
bbbb
|
|
||||||
<!-- <Component :is="footer[hostname]" /> -->
|
<!-- <Component :is="footer[hostname]" /> -->
|
||||||
</Body>
|
</Body>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
+1
-1
@@ -42,7 +42,7 @@ export default defineNuxtConfig({
|
|||||||
"~": resolve(__dirname, "./"),
|
"~": resolve(__dirname, "./"),
|
||||||
},
|
},
|
||||||
|
|
||||||
css: ["@/assets/styles/app.sass", "@unocss/reset/tailwind-compat.css"],
|
css: ["@/assets/styles/style.scss", "@/assets/styles/app.sass", "@unocss/reset/tailwind-compat.css"],
|
||||||
|
|
||||||
// dayjs
|
// dayjs
|
||||||
dayjs: {
|
dayjs: {
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import _cloneDeep from 'lodash/cloneDeep';
|
||||||
|
|
||||||
|
import DynamicTemplate from "~/components/dynamic-page/page/templates/index.vue";
|
||||||
|
import DynamicSection from "~/components/dynamic-page/page-section/templates/index.vue";
|
||||||
|
|
||||||
|
import { useDynamicPageStore } from '~/stores/dynamic-page';
|
||||||
|
const { currentPage, sectionPublished, componentPublished } = storeToRefs(useDynamicPageStore());
|
||||||
|
|
||||||
|
const store = reactive({
|
||||||
|
dynamicPage: useDynamicPageStore(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const loadData = async () => {
|
||||||
|
store.dynamicPage.fetchPageById(7);
|
||||||
|
|
||||||
|
store.dynamicPage.setSectionPublished();
|
||||||
|
store.dynamicPage.setComponentPublished()
|
||||||
|
}
|
||||||
|
await loadData()
|
||||||
|
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<main class="h-screen" v-if="currentPage">
|
||||||
|
<DynamicTemplate :settings="currentPage.settings">
|
||||||
|
<template v-if="sectionPublished && sectionPublished.length > 0">
|
||||||
|
<DynamicSection
|
||||||
|
v-for="(section, index) in sectionPublished"
|
||||||
|
:key="index"
|
||||||
|
:settings="section.settings"
|
||||||
|
:content="section.content ? JSON.parse(section.content) : null"
|
||||||
|
:section="section"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</DynamicTemplate>
|
||||||
|
</main>
|
||||||
|
</template>
|
||||||
+3
-1
@@ -19,7 +19,9 @@ const loadData = async () => {
|
|||||||
}
|
}
|
||||||
await loadData()
|
await loadData()
|
||||||
|
|
||||||
|
useHead({
|
||||||
|
title: 'Trang chủ'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
Reference in New Issue
Block a user