phongdt:header footer

This commit is contained in:
Duong Truong Phong
2024-05-30 22:57:45 +07:00
parent dd09434deb
commit 3b435e22ea
18 changed files with 787 additions and 467 deletions
@@ -1,237 +1,69 @@
<script setup lang="ts"> <script setup lang="ts">
</script> </script>
<template> <template>
<div> <footer class="border-t bg-white mt-6">
useCmsPageStore <div id="footer-desktop" class="px-4 mx-auto max-w-7xl 2xl:px-0 pt-4">
<div class="grid gap-4 font-semibold md:grid-cols-12 text-sm mb-2">
<div class="col-span-8">
<div class="grid grid-cols-2 md:grid-cols-3 gap-4">
</div> </div>
</div>
<div class="col-span-4 grid gap-4 sm:border-l sm:pl-4 auto-rows-max">
<div>
<p class="mb-2 uppercase text-xl font-bold">Liên hệ</p>
<div class="flex flex-col gap-3 whitespace-nowrap">
<div class="flex items-center max-w-full gap-2">
<Icon name="fa6-solid:building" />
<span class="text-sm hover-underline" title="Trụ sở chính: T.5 93A, Thụy Khuê, TP.Hà Nội">Toà Soạn</span>
</div>
<div class="flex items-center max-w-full gap-2">
<Icon name="fa6-solid:envelope" />
<a href="mailto:ktdtonline@gmail.com" class="text-sm hover-underline">
contact@vpress.vn
</a>
</div>
<div class="flex items-center max-w-full gap-2">
<Icon name="fa6-solid:handshake" />
<span class="text-sm">Hợp tác bản quyền</span>
</div>
</div>
</div>
<div>
<p class="mb-2 text-neutral-500">Đường dây nóng</p>
<div class="flex flex-col lg:(flex-row justify-between)">
<div class="flex flex-col">
<span class="text-lg font-bold tracking-wide">0123456789</span>
<p class="text-sm text-neutral-500">( Nội)</p>
</div>
<div class="flex flex-col">
<span class="text-lg font-bold tracking-wide">0123456789</span>
<p class="text-sm text-neutral-500">(Hồ Chí Minh)</p>
</div>
</div>
</div>
</div>
</div>
<hr />
<div class="flex flex-col items-center justify-between gap-4 my-2 sm:flex-row">
<div class="flex items-center justify-center sm:order-1">
<span>Hệ thống đang chạy thử nghiệm</span>
</div>
<div class="flex items-center justify-center gap-4 sm:order-3">
<a href="https://www.facebook.com" title="Theo dõi chúng tôi trên facebook" class="grid duration-300 border rounded-full w-9 h-9 border-neutral-200 text-neutral-500 place-items-center hover:bg-blue-500 hover:text-white hover:border-blue-500">
<Icon name="fa6-brands:facebook-f" />
</a>
<a href="https://www.youtube.com" title="Theo dõi chúng tôi trên youtube" class="grid duration-300 border rounded-full w-9 h-9 border-neutral-200 text-neutral-500 place-items-center hover:bg-black hover:text-white hover:border-black">
<Icon name="ion:logo-youtube" />
</a>
<a href="https://www.tiktok.com" title="Theo dõi chúng tôi trên tiktok" class="grid border rounded-full w-9 h-9 border-neutral-200 text-neutral-500 place-items-center hover:bg-black hover:text-white hover:border-black">
<Icon name="fa6-brands:tiktok" />
</a>
</div>
<div class="flex items-center justify-center gap-4 sm:ml-auto sm:order-2">
<a href="#!" class="text-sm lg:text-base text-neutral-500">RSS</a>
</div>
</div>
<hr />
</div>
</footer>
</template> </template>
<style lang="scss" scoped>
.col-span-8 {
grid-column: span 8 / span 8;
@media (max-width: 1150px) {
grid-column: span 7 / span 7;
}
}
.col-span-12 {
grid-column: span 12 / span 12 !important;
}
.mbootom-5 {
margin-bottom: 5px;
}
.mbootom-14 {
margin-bottom: 14px;
}
.text-neutral-500 {
color: #737373;
}
.grid-col-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
&.grid-col-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
.flex-col {
display: flex;
flex-direction: column;
.text-span {
font-size: 1rem;
line-height: 1.75rem;
font-weight: 700;
letter-spacing: 0.025em;
flex: 1;
word-break: break-word;
}
.text-a {
font-size: 0.8rem;
line-height: 1rem;
}
}
.lg-row {
@media (min-width: 1300px) {
flex-direction: row;
justify-content: space-between;
}
}
.footer1 {
margin-top: 1.5rem;
background-color: #ffffff;
color: black;
border-top: 1px solid #bfbfbf;
&-wrap {
max-width: 90%;
margin: auto;
padding-top: 1rem;
padding-left: 0;
padding-right: 0;
.section-right {
display: grid;
margin-bottom: 0.5rem;
gap: 1rem;
font-size: 0.8rem;
line-height: 1.25rem;
font-weight: 400;
@media (min-width: 950px) {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.footer-category {
display: grid;
gap: 1rem;
height: 100%;
/* grid-template-columns: repeat(5, minmax(0, 1fr)); */
&.grid-col-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
&.grid-col-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.item-nav {
padding: 10px;
.text {
font-size: 0.8rem;
line-height: 1.25rem;
color: black;
}
}
.drag-new {
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
background: #215486;
font-size: 40px;
color: #fff;
margin: 0 11px;
max-width: 200px;
height: 30px;
}
}
&-4 {
display: grid;
grid-column: span 4 / span 4;
grid-auto-rows: max;
gap: 1rem;
&.border-top-left-0 {
border-left: 0;
border-top: 1px solid #bfbfbf;
padding-top: 1rem;
}
@media (max-width: 1150px) {
grid-column: span 5 / span 5;
}
@media (min-width: 950px) {
padding-left: 1rem;
border-left: 1px solid #bfbfbf;
}
.text {
margin-bottom: 0.5rem;
font-size: 1rem;
line-height: 1.75rem;
font-weight: 700;
text-transform: uppercase;
&-wrap {
display: flex;
flex-direction: column;
gap: 0.75rem;
white-space: nowrap;
.text-item {
display: flex;
gap: 0.5rem;
align-items: center;
max-width: 100%;
.text-child {
font-size: 0.8rem;
line-height: 1.25rem;
flex: 1;
word-break: break-word;
}
}
}
}
}
}
.section-bottom {
display: flex;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
flex-direction: column;
gap: 1rem;
justify-content: space-between;
align-items: center;
@media (min-width: 640px) {
flex-direction: row;
}
.ssr {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
a {
font-size: 0.8rem;
color: #737373;
line-height: 1.25rem;
@media (min-width: 1024px) {
font-size: 1rem;
line-height: 1.5rem;
}
}
}
&__left {
display: flex;
justify-content: center;
align-items: center;
@media (min-width: 640px) {
order: 1;
}
}
&__right {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
.icon1 {
color: #737373;
display: grid;
place-items: center;
border-radius: 9999px;
border: 1px solid #737373;
transition-duration: 300ms;
width: 32px;
height: 32px;
}
@media (min-width: 640px) {
order: 3;
}
}
}
}
}
</style>
@@ -0,0 +1,15 @@
<script setup lang="ts">
import dayjs from 'dayjs';
const currentDateTime = ref<string>("");
onMounted(() => {
currentDateTime.value = dayjs().format("dddd, DD/MM/YYYY");
});
</script>
<template>
<div class="flex items-center text-sm whitespace-nowrap">
<Icon name="fa6-regular:clock" />
<span class="inline-block text-16px leading-normal ml-1">{{ currentDateTime }}</span>
</div>
</template>
@@ -1,237 +1,96 @@
<script setup lang="ts"> <script lang="ts" setup>
import { CurrentDateTime, LangSwitcher, TopNavigation, Mega } from "./index";
const widgetsStore = useWidgetsStore();
const layoutstore = useLayoutStore();
const { weather } = storeToRefs(widgetsStore);
const { megaMenuActive } = storeToRefs(layoutstore);
const navClass = ref("");
const handleScroll = () => {
if (window.scrollY > 0) {
navClass.value = "shadow-md";
} else {
navClass.value = "";
}
};
onMounted(async () => {
window.addEventListener("scroll", handleScroll);
await widgetsStore.fetchWeatherByLocation();
});
onUnmounted(() => {
window.removeEventListener("scroll", handleScroll);
});
</script> </script>
<template> <template>
<header id="header" class="relative">
<div class="w-full mx-auto px-4 max-w-8xl py-1">
<div id="top-bar-inner" class="flex items-center justify-between md:justify-center md:divide-x">
<NuxtLink to="/" id="logo" class="pr-6">
<img src="/images/200.png" alt="logo" class="object-cover w-24" />
</NuxtLink>
<ClientOnly>
<CurrentDateTime class="md:px-4 pt-5px" />
</ClientOnly>
<div class="items-center hidden px-6 ml-auto space-x-8 lg:flex">
<div> <div>
useCmsPageStore <ClientOnly>
<div v-if="weather" class="flex items-center space-x-1">
<p class="text-l">{{ weather.location.name }}</p>
<img :src="weather.current.condition.icon" alt="Weather Icon" class="h-8" />
<p class="text-l">{{ weather.current.temp_c }}°C</p>
</div> </div>
<div v-else>
<p>Đang tải thông tin thời tiết...</p>
</div>
</ClientOnly>
</div>
</div>
<div class="hidden md:flex gap-4 items-center px-4">
<button class="outline-none flex py-2 bg-transparent">
<Icon name="gg:search" size="18" />
</button>
<NuxtLink :to="`/subscriptions/paper`">
<Icon name="material-symbols:book-4-outline" />
</NuxtLink>
<!-- <Auth /> -->
<button class="outline-none flex py-2 bg-transparent">
<Icon name="fa6-regular:circle-user" size="16" />
</button>
</div>
<LangSwitcher class="hidden md:block px-4 pt-5px" />
<div class="xl:hidden block pl-4">
<button
type="button"
v-show="!megaMenuActive"
@click="layoutstore.setStatus(true)"
class="py-1 duration-300 hover:text-blue-500 bg-transparent"
>
<Icon name="fa6-solid:bars" />
</button>
<button
type="button"
v-show="megaMenuActive"
@click="layoutstore.setStatus(false)"
class="py-1 duration-300 hover:text-red-500 bg-transparent"
>
<Icon name="fa6-solid:xmark" />
</button>
</div>
</div>
</div>
</header>
<TopNavigation />
<Teleport to="body">
<Mega />
</Teleport>
</template> </template>
<style></style>
<style lang="scss" scoped>
.col-span-8 {
grid-column: span 8 / span 8;
@media (max-width: 1150px) {
grid-column: span 7 / span 7;
}
}
.col-span-12 {
grid-column: span 12 / span 12 !important;
}
.mbootom-5 {
margin-bottom: 5px;
}
.mbootom-14 {
margin-bottom: 14px;
}
.text-neutral-500 {
color: #737373;
}
.grid-col-2 {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
&.grid-col-1 {
grid-template-columns: repeat(1, minmax(0, 1fr));
}
}
.flex-col {
display: flex;
flex-direction: column;
.text-span {
font-size: 1rem;
line-height: 1.75rem;
font-weight: 700;
letter-spacing: 0.025em;
flex: 1;
word-break: break-word;
}
.text-a {
font-size: 0.8rem;
line-height: 1rem;
}
}
.lg-row {
@media (min-width: 1300px) {
flex-direction: row;
justify-content: space-between;
}
}
.footer1 {
margin-top: 1.5rem;
background-color: #ffffff;
color: black;
border-top: 1px solid #bfbfbf;
&-wrap {
max-width: 90%;
margin: auto;
padding-top: 1rem;
padding-left: 0;
padding-right: 0;
.section-right {
display: grid;
margin-bottom: 0.5rem;
gap: 1rem;
font-size: 0.8rem;
line-height: 1.25rem;
font-weight: 400;
@media (min-width: 950px) {
grid-template-columns: repeat(12, minmax(0, 1fr));
}
.footer-category {
display: grid;
gap: 1rem;
height: 100%;
/* grid-template-columns: repeat(5, minmax(0, 1fr)); */
&.grid-col-3 {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
&.grid-col-2 {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.item-nav {
padding: 10px;
.text {
font-size: 0.8rem;
line-height: 1.25rem;
color: black;
}
}
.drag-new {
display: flex;
justify-content: center;
align-items: center;
border-radius: 6px;
background: #215486;
font-size: 40px;
color: #fff;
margin: 0 11px;
max-width: 200px;
height: 30px;
}
}
&-4 {
display: grid;
grid-column: span 4 / span 4;
grid-auto-rows: max;
gap: 1rem;
&.border-top-left-0 {
border-left: 0;
border-top: 1px solid #bfbfbf;
padding-top: 1rem;
}
@media (max-width: 1150px) {
grid-column: span 5 / span 5;
}
@media (min-width: 950px) {
padding-left: 1rem;
border-left: 1px solid #bfbfbf;
}
.text {
margin-bottom: 0.5rem;
font-size: 1rem;
line-height: 1.75rem;
font-weight: 700;
text-transform: uppercase;
&-wrap {
display: flex;
flex-direction: column;
gap: 0.75rem;
white-space: nowrap;
.text-item {
display: flex;
gap: 0.5rem;
align-items: center;
max-width: 100%;
.text-child {
font-size: 0.8rem;
line-height: 1.25rem;
flex: 1;
word-break: break-word;
}
}
}
}
}
}
.section-bottom {
display: flex;
margin-top: 0.5rem;
margin-bottom: 0.5rem;
flex-direction: column;
gap: 1rem;
justify-content: space-between;
align-items: center;
@media (min-width: 640px) {
flex-direction: row;
}
.ssr {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
a {
font-size: 0.8rem;
color: #737373;
line-height: 1.25rem;
@media (min-width: 1024px) {
font-size: 1rem;
line-height: 1.5rem;
}
}
}
&__left {
display: flex;
justify-content: center;
align-items: center;
@media (min-width: 640px) {
order: 1;
}
}
&__right {
display: flex;
gap: 1rem;
justify-content: center;
align-items: center;
.icon1 {
color: #737373;
display: grid;
place-items: center;
border-radius: 9999px;
border: 1px solid #737373;
transition-duration: 300ms;
width: 32px;
height: 32px;
}
@media (min-width: 640px) {
order: 3;
}
}
}
}
}
</style>
@@ -0,0 +1,37 @@
<script setup lang="ts">
import { onClickOutside } from "@vueuse/core";
const langSwitcherEl = ref<HTMLDivElement>();
const selectingLanguages = ref<boolean>(false);
const classes = computed(() => ({
"pointer-events-auto opacity-100": selectingLanguages.value,
"pointer-events-none opacity-0": !selectingLanguages.value,
}));
onClickOutside(langSwitcherEl, () => selectingLanguages.value = false);
const languages = ['Tiếng việt']
const onSelectLanguage = () => selectingLanguages.value = false
</script>
<template>
<div ref="langSwitcherEl" id="lang-switcher" class="relative text-sm">
<button class="text-sm bg-transparent" @click="selectingLanguages = !selectingLanguages">
Tiếng việt
</button>
<div id="languages-switchable" :class="classes"
class="absolute z-50 min-w-36 right-0 top-10 bg-white rounded shadow overflow-hidden shadow-lg flex flex-col duration-300">
<div class="relative w-full px-1 py-1">
<ul>
<li v-for="(l, i) in languages" @click="onSelectLanguage" :key="i">
<button class="py-2 w-full rounded duration-300 hover:bg-blue-400 hover:text-white">
{{ l }}
</button>
</li>
</ul>
</div>
</div>
</div>
</template>
@@ -0,0 +1,61 @@
<script setup lang="ts">
import { onClickOutside } from "@vueuse/core";
import { useNavigationStoreV2 } from '~/stores/navigation';
import {storeToRefs} from "pinia";
import { vInterpolate } from '~/directives/v-interpolate';
import * as cherrio from 'cheerio'
const router = useRouter();
const route = useRoute();
const v2NavigationStore = useNavigationStoreV2()
const layoutstore = useLayoutStore();
const { megaMenuActive } = storeToRefs(layoutstore);
const {topMenu} = storeToRefs(v2NavigationStore)
const megaMenuEl = ref<HTMLElement>();
const computedClass = computed(() =>
megaMenuActive.value
? ["opacity-100", "pointer-events-auto"]
: ["opacity-0", "pointer-events-none"]
);
const $ = cherrio.load(topMenu.value)
const html = $('.parent').addClass('xl:(flex items-center justify-center)')
html.find('>li').addClass('xl:(relative group xl:mr-3) hover:bg-[#e6f4ff] py-3 px-6 rounded-md')
html.find('ul').addClass('pl-4 hidden xl:(gap-0 w-200px shadow group-hover:(block absolute top-full left-0 bg-white z-50))')
html.find('>li>a').addClass('xl:(block py-4 hover:(text-blue))')
html.find('>li>ul>li>a').addClass('xl:(block py-10px px-15px hover:(bg-blue text-white))')
</script>
<template>
<a-drawer
v-model:open="megaMenuActive"
class="custom-class"
root-class-name="root-class-name"
title="Tất cả chuyên mục"
placement="right"
:bodyStyle="{padding:0}"
>
<div
class=" h-full max-h-full flex flex-col gap-y-4 mx-auto"
>
<div id="mega-menu" v-interpolate v-html="html"></div>
</div>
</a-drawer>
</template>
<style lang="sass" scoped>
#mega-menu
max-height: 100vh
min-height: 100vh
#mega-list
max-height: 100vh
</style>
@@ -0,0 +1,23 @@
<script setup lang="ts">
import { useNavigationStoreV2 } from '~/stores/navigation';
import {storeToRefs} from "pinia";
import { vInterpolate } from '~/directives/v-interpolate';
import * as cherrio from 'cheerio'
const v2NavigationStore = useNavigationStoreV2()
const {topMenu} = storeToRefs(v2NavigationStore)
await v2NavigationStore.fetchNavigation()
const $ = cherrio.load(topMenu.value)
const html = $('.parent').addClass('xl:(flex items-center justify-center)')
html.find('>li').addClass('relative group xl:mr-3 hover:text-blue')
html.find('ul').addClass('hidden w-200px shadow group-hover:(block absolute top-full left-0 bg-white z-50)')
html.find('>li>a').addClass('block py-4')
html.find('>li>ul>li>a').addClass('block py-10px px-15px text-black hover:(bg-blue text-white)')
</script>
<template>
<nav class="main-nav text-sm z-40 sticky top-0 bg-white relative border-y border-neutral-200 hidden xl:block" v-interpolate v-html="html">
</nav>
</template>
@@ -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'
+53
View File
@@ -0,0 +1,53 @@
import type { ObjectDirective } from 'vue'
type InterpolationElement = HTMLElement & {
$componentUpdated?: () => void
$destroy?: () => void
}
export const vInterpolate: ObjectDirective<InterpolationElement> = {
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('/')
}
+2
View File
@@ -1,5 +1,6 @@
import { createRouter, defineEventHandler, useBase } from 'h3' import { createRouter, defineEventHandler, useBase } from 'h3'
import * as DynamicPageCtrl from '~/server/models/dynamic-page' import * as DynamicPageCtrl from '~/server/models/dynamic-page'
import * as navigationCtrl from '~/server/models/navigation'
const router = createRouter() const router = createRouter()
@@ -26,5 +27,6 @@ router.get('/get-by-id/:id', defineEventHandler(async (event : any) => {
handleError(error); handleError(error);
} }
})) }))
router.get('/navigation', defineEventHandler(navigationCtrl.get))
export default useBase('/api/services', router.handler) export default useBase('/api/services', router.handler)
+6
View File
@@ -0,0 +1,6 @@
export default interface Base {
createdBy?: string | number
createdOn?: string
updatedBy?: string | number
updatedOn?: string
}
+44
View File
@@ -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);
}
};
+99
View File
@@ -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 = `
<ul class="parent">
<li><a href="/thoi-su" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Thời sự</a>
<ul>
<li><a href="/tin-tuc" data-title="data-description=" data-description="data-code=data-keyword=" data-code="data-keyword=data-target=_blank" data-target="_blank" data-type="data-feature=true" data-feature="true">Tin tức</a></li>
<li><a href="/thong-tin-doi-ngoai" data-title="data-description=" data-description="data-code=data-keyword=" data-code="data-keyword=data-target=_blank" data-keyword="data-target=_blank" data-type="data-feature=true" data-feature="true">Thông tin đối ngoại</a></li>
</ul>
</li>
<li><a href="/kinh-te" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Kinh tế</a>
<ul>
<li><a href="/thi-truong" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Thị trường</a></li>
<li><a href="/hang-viet" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Hàng việt</a></li>
</ul>
</li>
<li><a href="/do-thi" data-title="data-description=" data-description="data-code=data-keyword=" data-code="data-keyword=data-target=_blank" data-target="_blank" data-type="data-feature=true" data-feature="true">Đô thị</a>
<ul>
<li><a href="/do-thi-24h" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Đô thị 24h</a></li>
<li><a href="/giao-thong" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Giao thông</a></li>
</ul>
</li>
<li><a href="/bat-dong-san" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Bất động sản</a>
<ul>
<li><a href="/thi-truong" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Thị trường</a></li>
<li><a href="/tu-van-dau-tu" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Tư vấn đầu tư</a></li>
<li><a href="/du-an" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Dự án</a></li>
</ul>
</li>
<li><a href="/y-te" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Y tế</a>
<ul>
<li><a href="/an-toan-thuc-pham" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">An toàn thực phẩm</a></li>
<li><a href="/tu-van-suc-khoe" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Tư vấn sức khỏe</a></li>
</ul>
</li>
<li><a href="/giao-duc" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Giáo dục</a>
<ul>
<li><a href="/tuyen-sinh" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Tuyển sinh</a></li>
<li><a href="/cau-chuyen-hoc-duong" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Câu chuyên học đường</a></li>
</ul>
</li>
<li><a href="/doi-song" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Đời sống</a>
<ul>
<li><a href="/viec-lam-an-sinh-xa-hoi" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Việc làm - an sinh xã hội</a></li>
<li><a href="/phong-su-ghi-chep" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Phóng sự ghi chép</a></li>
</ul>
</li>
<li><a href="/van-hoa" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Văn hóa</a>
<ul>
<li><a href="/van-nghe" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Văn nghệ</a></li>
<li><a href="/ha-noi-thanh-lich-van-minh" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Hà Nội thanh lịch văn minh</a></li>
</ul>
</li>
<li><a href="/phap-luat" data-title="data-description=" data-description="data-code=data-keyword=" data-keyword="data-target=_blank" data-type="data-feature=true">Pháp luật</a>
<ul>
<li><a href="/pha-an" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Phá án</a></li>
<li><a href="/phap-dinh" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Pháp đình</a></li>
</ul>
</li>
<li><a href="/quoc-te" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Quốc tế</a>
<ul>
<li><a href="/quoc-te-24h" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Quốc tế 24h</a></li>
<li><a href="/kinh-te-tai-chinh-toan-cau" data-title="data-description=" data-code="data-keyword=" data-target="data-type=" data-feature="true">Kinh tế tài chính toàn cầu</a></li>
</ul>
</li>
<li> <a href="/multimedia" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Multimedia</a>
<ul>
<li><a href="/podcast" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Podcast</a></li>
<li><a href="/anh" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Ảnh</a></li>
<li><a href="/video-clip" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Video Clip</a></li>
<li><a href="/infographics" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Infographics</a></li>
<li><a href="/emagazine" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Emagazine</a></li>
</ul>
</li>
<li> <a href="/chuyen-doi-so" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Chuyển đổi số</a>
<ul>
<li><a href="/cong-nghe" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Công nghệ</a></li>
<li><a href="/trai-nghiem" data-title="" data-description="" data-code="" data-keyword="" data-target="" data-type="1" data-feature="true">Trải nghiệm</a></li>
</ul>
</li>
</ul>
`
export const get = () =>{
return navigation;
}
+73
View File
@@ -0,0 +1,73 @@
import type { Category } from "~/server/models/category";
export const useCategoryStore = defineStore("category-v2", () => {
const categories = ref<Category[]>([]);
async function fetchCategories() {
const { data, error } = await useFetch<Category[]>("/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));
}
+13
View File
@@ -0,0 +1,13 @@
import { defineStore, acceptHMRUpdate } from "pinia";
export const useLayoutStore = defineStore("layout", () => {
const megaMenuActive = ref<boolean>(false);
function setStatus(status: boolean) {
megaMenuActive.value = status;
}
return { megaMenuActive, setStatus };
});
if (import.meta.hot) {
import.meta.hot.accept(acceptHMRUpdate(useLayoutStore, import.meta.hot));
}
+24
View File
@@ -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))
}
+26
View File
@@ -0,0 +1,26 @@
import { defineStore, acceptHMRUpdate } from 'pinia'
export const useWidgetsStore = defineStore('widgets', () => {
const weather = ref<any>(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))
}
+149
View File
@@ -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();
});
}