diff --git a/components/dynamic-page/page/templates/components/footers/FooterHomeTemplate.vue b/components/dynamic-page/page/templates/components/footers/FooterHomeTemplate.vue index e3b465e..28ee002 100644 --- a/components/dynamic-page/page/templates/components/footers/FooterHomeTemplate.vue +++ b/components/dynamic-page/page/templates/components/footers/FooterHomeTemplate.vue @@ -1,237 +1,69 @@ - - diff --git a/components/dynamic-page/page/templates/components/headers/CurrentDateTime.vue b/components/dynamic-page/page/templates/components/headers/CurrentDateTime.vue new file mode 100644 index 0000000..07f3940 --- /dev/null +++ b/components/dynamic-page/page/templates/components/headers/CurrentDateTime.vue @@ -0,0 +1,15 @@ + + + diff --git a/components/dynamic-page/page/templates/components/headers/HeaderHomeTemplate.vue b/components/dynamic-page/page/templates/components/headers/HeaderHomeTemplate.vue index e3b465e..79f7878 100644 --- a/components/dynamic-page/page/templates/components/headers/HeaderHomeTemplate.vue +++ b/components/dynamic-page/page/templates/components/headers/HeaderHomeTemplate.vue @@ -1,237 +1,96 @@ - - - + diff --git a/components/dynamic-page/page/templates/components/headers/LangSwitcher.vue b/components/dynamic-page/page/templates/components/headers/LangSwitcher.vue new file mode 100644 index 0000000..492b805 --- /dev/null +++ b/components/dynamic-page/page/templates/components/headers/LangSwitcher.vue @@ -0,0 +1,37 @@ + + + diff --git a/components/dynamic-page/page/templates/components/headers/Mega.vue b/components/dynamic-page/page/templates/components/headers/Mega.vue new file mode 100644 index 0000000..c24bb55 --- /dev/null +++ b/components/dynamic-page/page/templates/components/headers/Mega.vue @@ -0,0 +1,61 @@ + + + + + diff --git a/components/dynamic-page/page/templates/components/headers/TopNavigation.vue b/components/dynamic-page/page/templates/components/headers/TopNavigation.vue new file mode 100644 index 0000000..a8bbfa5 --- /dev/null +++ b/components/dynamic-page/page/templates/components/headers/TopNavigation.vue @@ -0,0 +1,23 @@ + + + \ No newline at end of file diff --git a/components/dynamic-page/page/templates/components/headers/index.ts b/components/dynamic-page/page/templates/components/headers/index.ts new file mode 100644 index 0000000..804c00e --- /dev/null +++ b/components/dynamic-page/page/templates/components/headers/index.ts @@ -0,0 +1,4 @@ +export { default as LangSwitcher } from './LangSwitcher.vue' +export { default as CurrentDateTime } from './CurrentDateTime.vue' +export { default as TopNavigation } from './TopNavigation.vue' +export { default as Mega } from './Mega.vue' \ No newline at end of file diff --git a/directives/v-interpolate.ts b/directives/v-interpolate.ts new file mode 100644 index 0000000..ecec097 --- /dev/null +++ b/directives/v-interpolate.ts @@ -0,0 +1,53 @@ +import type { ObjectDirective } from 'vue' + +type InterpolationElement = HTMLElement & { + $componentUpdated?: () => void + $destroy?: () => void +} + +export const vInterpolate: ObjectDirective = { + mounted(el) { + const links = Array.from(el.getElementsByTagName('a')).filter((linkEl) => { + const href = linkEl.getAttribute('href') + if (!href) { + return false + } + + return isInternalLink(href) + }) + + addListeners(links) + // cleanup + el.$componentUpdated = () => { + removeListeners(links) + nextTick(() => addListeners(links)) + } + el.$destroy = () => removeListeners(links) + }, + updated: (el) => el.$componentUpdated?.(), + beforeUnmount: (el) => el.$destroy?.() +} + + +function navigate(event: Event) { + const target = event.target as HTMLElement + const href = target.getAttribute('href') + event.preventDefault() + return navigateTo(href) +} + +function addListeners(links: HTMLAnchorElement[]) { + links.forEach((link) => { + link.addEventListener('click', navigate, false) + }) +} + +function removeListeners(links: HTMLAnchorElement[]) { + links.forEach((link) => { + link.removeEventListener('click', navigate, false) + }) +} + +function isInternalLink(href?: string) { + return href?.startsWith('/') +} \ No newline at end of file diff --git a/server/api/services/[...].ts b/server/api/services/[...].ts index ecdacc1..0919023 100644 --- a/server/api/services/[...].ts +++ b/server/api/services/[...].ts @@ -1,5 +1,6 @@ import { createRouter, defineEventHandler, useBase } from 'h3' import * as DynamicPageCtrl from '~/server/models/dynamic-page' +import * as navigationCtrl from '~/server/models/navigation' const router = createRouter() @@ -26,5 +27,6 @@ router.get('/get-by-id/:id', defineEventHandler(async (event : any) => { handleError(error); } })) +router.get('/navigation', defineEventHandler(navigationCtrl.get)) export default useBase('/api/services', router.handler) diff --git a/server/models/base.ts b/server/models/base.ts new file mode 100644 index 0000000..9a3f902 --- /dev/null +++ b/server/models/base.ts @@ -0,0 +1,6 @@ +export default interface Base { + createdBy?: string | number + createdOn?: string + updatedBy?: string | number + updatedOn?: string +} \ No newline at end of file diff --git a/server/models/category.ts b/server/models/category.ts new file mode 100644 index 0000000..a20cd85 --- /dev/null +++ b/server/models/category.ts @@ -0,0 +1,44 @@ +import Base from "./base"; +import {H3Event} from "h3"; + +export type Category = { + id: number; + siteId: number; + parentId?: number; + title: string; + code: string; + description?: string; + thumbnail?: string; + keyword?: string; + taxonomy?: string; + type: number; + layout?: number; + template?: string; + feature?: string; + settings?: string; + order?: number; + isPublished?: boolean; + publishType?: number; + publishedBy?: string; + publishedOn?: string; + status: number; +} & Base; + +export const list = async () => { + try { + const { site, apiUrl } = useRuntimeConfig().public; + + const {items}:any = await $fetch(`${apiUrl}/cms/category/site`, { + method: "GET", + headers: { + "Content-Type": "application/json", + Accept: "application/json", + site: site, + }, + }); + + return items; + } catch (error) { + handleError(error); + } +}; diff --git a/server/models/navigation.ts b/server/models/navigation.ts new file mode 100644 index 0000000..40f61fb --- /dev/null +++ b/server/models/navigation.ts @@ -0,0 +1,99 @@ +import Base from "./base"; + +/** + * Represents a navigation item. + */ +export type NavigationItem = { + id: number; + siteId: number; + title: string; + content: string; + feature?: string; + taxonomy?: string; + status: number; +} & Base; + +const navigation = ` + +` + +export const get = () =>{ + return navigation; +} \ No newline at end of file diff --git a/stores/category.ts b/stores/category.ts new file mode 100644 index 0000000..26b0a2d --- /dev/null +++ b/stores/category.ts @@ -0,0 +1,73 @@ +import type { Category } from "~/server/models/category"; + +export const useCategoryStore = defineStore("category-v2", () => { + const categories = ref([]); + + async function fetchCategories() { + const { data, error } = await useFetch("/api/v2/categories"); + if (error.value) { + return [] as Category[]; + } + + categories.value = Object.assign([], data.value); + + return categories.value; + } + + function findByCode(code?: string) { + if (code) return categories.value.find((c) => c.code === code); + } + + function findById(id?: number) { + return categories.value.find((c) => c.id === id); + } + + function findParents(category?: Category) { + if (!category) return []; + + const parents = []; + let parent = findById(category.parentId); + while (parent) { + parents.push(parent); + parent = findById(parent.parentId); + } + + return parents.reverse().concat(category); + } + + function findSubTree(category?: Category) { + if (!category) return []; + + let subTree = [] as Category[]; + + function findChildren(category: Category) { + const children = categories.value.filter((c:Category) => c.parentId === category.id); + if (children.length === 0) return; + + subTree.push(...children,category); + } + + if(category.parentId === 41){ + findChildren(category); + }else{ + const parent = findById(category.parentId); + if(parent){ + findChildren(parent); + } + } + + return subTree.reverse(); + } + + function findChildren(category: Category) { + const children = categories.value.filter((c:Category) => c.parentId === category.id); + if (children.length === 0) return; + else return [...children] + } + + return { categories, fetchCategories, findByCode, findById, findParents,findSubTree, findChildren }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useCategoryStore, import.meta.hot)); +} diff --git a/stores/layout.ts b/stores/layout.ts new file mode 100644 index 0000000..86e5ec5 --- /dev/null +++ b/stores/layout.ts @@ -0,0 +1,13 @@ +import { defineStore, acceptHMRUpdate } from "pinia"; + +export const useLayoutStore = defineStore("layout", () => { + const megaMenuActive = ref(false); + function setStatus(status: boolean) { + megaMenuActive.value = status; + } + return { megaMenuActive, setStatus }; +}); + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useLayoutStore, import.meta.hot)); +} diff --git a/stores/navigation.ts b/stores/navigation.ts new file mode 100644 index 0000000..092a4f7 --- /dev/null +++ b/stores/navigation.ts @@ -0,0 +1,24 @@ +import { defineStore, acceptHMRUpdate } from 'pinia' + +export const useNavigationStoreV2 = defineStore('navigation-v2', () => { + const topMenu = ref('') + +async function fetchNavigation() { + const {data, error } = await useFetch('/api/services/navigation') + + if (error.value) { + return '' + } + if(data.value) { + topMenu.value = data.value + } + + return topMenu.value +} + + return {topMenu, fetchNavigation} +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useNavigationStoreV2, import.meta.hot)) +} diff --git a/stores/widgets.ts b/stores/widgets.ts new file mode 100644 index 0000000..034718e --- /dev/null +++ b/stores/widgets.ts @@ -0,0 +1,26 @@ +import { defineStore, acceptHMRUpdate } from 'pinia' + +export const useWidgetsStore = defineStore('widgets', () => { + const weather = ref(null) + const locations = ref(["Hanoi", "Ho Chi Minh City", "Huế", "Danang", "Hai Phong", "Nha Trang"]) + const selectedLocation = ref("Hanoi") + + async function fetchWeatherByLocation(location?:string){ + try { + if(!location){ + location = selectedLocation.value + } + const response = await $fetch( + `https://api.weatherapi.com/v1/current.json?key=56e1a8576f0c482280d84625230905&q=${location}&aqi=yes` + ); + weather.value = response; + } catch (error) { + console.error(error); + } + } + return {locations,selectedLocation,weather,fetchWeatherByLocation} +}) + +if (import.meta.hot) { + import.meta.hot.accept(acceptHMRUpdate(useWidgetsStore, import.meta.hot)) +} diff --git a/server/utils/error.ts b/utils/error.ts similarity index 100% rename from server/utils/error.ts rename to utils/error.ts diff --git a/utils/utilities.ts b/utils/utilities.ts new file mode 100644 index 0000000..0c46edd --- /dev/null +++ b/utils/utilities.ts @@ -0,0 +1,149 @@ +import * as cherrio from "cheerio"; + +export const utils = { + toNumber, + toString, + toBoolean, + toNumberArray, + toStringArray, + dateFormat, + generateSlugWithId, + formattedTime, + formateDate, + isDev, + domainImage, + uid, + isExternalUrl, + isValidPhone, + isTouchDevice, + toTitleCase +}; + +function toNumber(value: any, _default?: number): number { + const number = parseInt(String(value)); + return Number( + isNaN(number) ? (_default !== undefined ? _default : 0) : number + ); +} + +function toString(value: any): string { + return String(value); +} + +function toBoolean(value: any): boolean { + const lowercaseValue = String(value).toLowerCase(); + if (lowercaseValue === "true") { + return true; + } else if (lowercaseValue === "false") { + return false; + } else { + throw new Error("Invalid boolean string"); + } +} + +function toNumberArray(value: any): number[] { + return String(value) + .split(",") + .map((item) => Number(item.trim())) + .filter((num) => !isNaN(num)); +} +function toStringArray(value: any): string[] { + return String(value) + .split(",") + .map((item) => item.trim()); +} + +import dayjs from "dayjs"; + +function dateFormat(date: any, format?: string) { + const dayjsInstance = dayjs(date); + const formatter = format ?? "ddd, D MMM YYYY HH:mm"; + let d = dayjsInstance.format(formatter); + return d; +} + +function generateSlugWithId(prefix?: string, slug?: string, id?: number) { + return `${prefix}/${slug}${id ? "-" + id : ""}`; +} + +function formattedTime(seconds: number) { + const minutes = Math.floor(seconds / 60); + const remainingSeconds = Math.floor(seconds % 60); + return `${String(minutes).padStart(2, "0")}:${String( + remainingSeconds + ).padStart(2, "0")}`; +} + +function formateDate( + date: string | Date | undefined, + formatter: string = "HH:mm, dddd, D MMMM YYYY" +) { + if (!date) { + return ""; + } + const formattedDate = useDateFormat(date, formatter, { locales: "vi-VN" }); + + const dateObject = new Date(date); + const time = formattedDate.value.slice(0, 5); // Extract HH:mm + const day = `0${dateObject.getDate()}`.slice(-2); // Get day with leading zero + const month = `0${dateObject.getMonth() + 1}`.slice(-2); // Get month with leading zero + const year = dateObject.getFullYear(); + + // Creating the desired format "16:21 | 04/10/2022" + const result = ` ${day}/${month}/${year}`; + + return result; +} + +function isDev() { + return process.env.NODE_ENV === "development"; +} + + +function domainImage(text: string = "", domainImage: string = "") { + const replaceDomains = ["http://45.77.168.121:8083"]; + + if (text) { + const $ = cherrio.load(text, null, false); + + $("figure img").each((i, el) => { + const src = $(el).attr("src"); + + if (src && replaceDomains.some((domain) => src.startsWith(domain))) { + const replaceDomain = replaceDomains.find((domain) => + src.startsWith(domain) + )!; + $(el).attr("src", src.replace(replaceDomain, domainImage)); + } + }); + + return $.html(); + } +} + +let _id = 0 + +function uid () { + _id = (_id + 1) % Number.MAX_SAFE_INTEGER + return `vuid-${_id}` +} + +function isExternalUrl(url?: string) { + if(!url) return false + return /^(http?:|https?:|mailto:|tel:)/.test(url) +} + +function isValidPhone(phone:string){ + return /^(0)(3[2-9]|5[6|8|9]|7[0|6-9]|8[0-6|8|9]|9[0-4|6-9])[0-9]{7}$/.test(phone) +} + +function isTouchDevice() { + return 'maxTouchPoints' in navigator && navigator.maxTouchPoints > 0; +} + +function toTitleCase(str?: string){ + if (!str) return; + return str.replace(/\w\S*/g, function (txt) { + return txt.charAt(0).toUpperCase() + txt.substring(1).toLowerCase(); + }); +} \ No newline at end of file