Merge pull request 'phongdt:header footer' (#1) from phongdt into main
Reviewed-on: http://work.gct.com.vn/minhnt/NSG_PORTAL_V2/pulls/1
This commit was merged in pull request #1.
This commit is contained in:
@@ -1,237 +1,69 @@
|
||||
<script setup lang="ts">
|
||||
</script>
|
||||
<template>
|
||||
<div>
|
||||
useCmsPageStore
|
||||
<footer class="border-t bg-white mt-6">
|
||||
<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 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">(Hà 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>
|
||||
|
||||
<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>
|
||||
<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>
|
||||
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 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>
|
||||
|
||||
<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>
|
||||
<style></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'
|
||||
@@ -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('/')
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
export default interface Base {
|
||||
createdBy?: string | number
|
||||
createdOn?: string
|
||||
updatedBy?: string | number
|
||||
updatedOn?: string
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user