feat: implement Contact Us page with dynamic database seeding and dedicated view controller logic

This commit is contained in:
2026-07-20 19:29:47 +07:00
parent 27fd9e109d
commit a8220ec02d
147 changed files with 12277 additions and 39 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,182 @@
<section class="xl:py-16 md:py-12 py-8 md:scroll-mt-[186px]" id="map-section">
<div class="container space-y-4 md:space-y-6 xl:space-y-8">
<h1 class="text-primary-600 display-7 text-center uppercase">Liên hệ</h1>
<div class="grid grid-cols-12 gap-6">
<div class="col-span-full lg:col-span-5 xl:col-span-4 rounded-lg pr-2 pl-4 py-4 bg-gray-100">
<div class="space-y-4 overflow-y-auto h-[500px] pr-2">
<div class="rounded-lg overflow-hidden cursor-pointer bg-primary-600">
<div class="xl:p-4 p-3 space-y-2">
<div class="title-1 text-white">Cơ sở 1</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-4 flex-shrink-0 text-white mt-1">
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0">
</path>
<circle cx="12" cy="10" r="3">
</circle>
</svg>
<a href="https://maps.app.goo.gl/TmwiUqbghLDDNybs8" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:duration-150 lg:hover:underline underline-offset-4 text-white">215 Hồng Bàng, Phường Chợ Lớn, Thành phố Hồ Chí Minh</a>
</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone size-4 flex-shrink-0 text-white mt-1">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z">
</path>
</svg>
<a href="tel:(84.28) 3855 4269" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-white">(84.28) 3855 4269</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail size-4 flex-shrink-0 text-white">
<rect width="20" height="16" x="2" y="4" rx="2">
</rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7">
</path>
</svg>
<a href="mailto:bvdhyd@umc.edu.vn" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-white">bvdhyd@umc.edu.vn</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-4 flex-shrink-0 text-white">
<circle cx="12" cy="12" r="10">
</circle>
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20">
</path>
<path d="M2 12h20">
</path>
</svg>
<a href="https://www.bvdaihoc.com.vn/" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-white">https://www.bvdaihoc.com.vn</a>
</div>
</div>
</div>
<div class="rounded-lg overflow-hidden cursor-pointer bg-white">
<div class="xl:p-4 p-3 space-y-2">
<div class="title-1 text-primary-600">Cở sở 2</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-4 flex-shrink-0 text-primary-600 mt-1">
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0">
</path>
<circle cx="12" cy="10" r="3">
</circle>
</svg>
<a href="https://maps.app.goo.gl/wnEnwkmDKoXiNbvU8" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:duration-150 lg:hover:underline underline-offset-4 text-gray-900">201 Nguyễn Chí Thanh, Phường Chợ Lớn, Thành phố Hồ Chí Minh</a>
</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone size-4 flex-shrink-0 text-primary-600 mt-1">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z">
</path>
</svg>
<a href="tel:(84.28) 3955 5548" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">(84.28) 3955 5548</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail size-4 flex-shrink-0 text-primary-600">
<rect width="20" height="16" x="2" y="4" rx="2">
</rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7">
</path>
</svg>
<a href="mailto:bvdaihoccoso2@umc.edu.vn" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">bvdaihoccoso2@umc.edu.vn</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-4 flex-shrink-0 text-primary-600">
<circle cx="12" cy="12" r="10">
</circle>
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20">
</path>
<path d="M2 12h20">
</path>
</svg>
<a href="https://www.bvdaihoccoso2.com.vn/" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">https://www.bvdaihoccoso2.com.vn</a>
</div>
</div>
</div>
<div class="rounded-lg overflow-hidden cursor-pointer bg-white">
<div class="xl:p-4 p-3 space-y-2">
<div class="title-1 text-primary-600">Cơ sở 3</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-4 flex-shrink-0 text-primary-600 mt-1">
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0">
</path>
<circle cx="12" cy="10" r="3">
</circle>
</svg>
<a href="https://maps.app.goo.gl/x2XUmcC7yJvRq3FU9" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:duration-150 lg:hover:underline underline-offset-4 text-gray-900">221B Hoàng Văn Thụ, Phường Phú Nhuận, Thành phố Hồ Chí Minh</a>
</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone size-4 flex-shrink-0 text-primary-600 mt-1">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z">
</path>
</svg>
<a href="tel:(84.28) 3842 0070" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">(84.28) 3842 0070</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail size-4 flex-shrink-0 text-primary-600">
<rect width="20" height="16" x="2" y="4" rx="2">
</rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7">
</path>
</svg>
<a href="mailto:bvdaihoccoso3@umc.edu.vn" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">bvdaihoccoso3@umc.edu.vn</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-4 flex-shrink-0 text-primary-600">
<circle cx="12" cy="12" r="10">
</circle>
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20">
</path>
<path d="M2 12h20">
</path>
</svg>
<a href="https://www.bvdaihoccoso3.com.vn/" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">https://www.bvdaihoccoso3.com.vn</a>
</div>
</div>
</div>
<div class="rounded-lg overflow-hidden cursor-pointer bg-white">
<div class="xl:p-4 p-3 space-y-2">
<div class="title-1 text-primary-600">Đơn vị hợp tác</div>
<div class="title-1 text-black">Phòng khám Bệnh viện Đại học Y Dược 1</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-4 flex-shrink-0 text-primary-600 mt-1">
<path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0">
</path>
<circle cx="12" cy="10" r="3">
</circle>
</svg>
<a href="https://maps.google.com/?q=20-22+D%C6%B0%C6%A1ng+Quang+Trung,+Ph%C6%B0%E1%BB%9Dng+12,+Qu%E1%BA%ADn+10,+Th%C3%A0nh+ph%E1%BB%91+H%E1%BB%93+Ch%C3%AD+Minh" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:duration-150 lg:hover:underline underline-offset-4 text-gray-900">20-22 Dương Quang Trung, Phường Hòa Hưng, Thành phố Hồ Chí Minh</a>
</div>
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone size-4 flex-shrink-0 text-primary-600 mt-1">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z">
</path>
</svg>
<a href="tel:1900 6923" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">1900 6923</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail size-4 flex-shrink-0 text-primary-600">
<rect width="20" height="16" x="2" y="4" rx="2">
</rect>
<path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7">
</path>
</svg>
<a href="mailto:contact.us@umcclinic.com.vn" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">contact.us@umcclinic.com.vn</a>
</div>
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-4 flex-shrink-0 text-primary-600">
<circle cx="12" cy="12" r="10">
</circle>
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20">
</path>
<path d="M2 12h20">
</path>
</svg>
<a href="https://umcclinic.com.vn/" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 text-gray-900">https://umcclinic.com.vn/</a>
</div>
</div>
</div>
</div>
</div>
<div class="col-span-full lg:col-span-7 xl:col-span-8">
<div class="relative rounded-lg overview-wrapper-shadow overflow-hidden w-full h-full max-lg:h-[450px] [&amp;&gt;iframe]:w-full [&amp;&gt;iframe]:h-full [&amp;&gt;iframe]:border-0">
<iframe src="./Liên hệ_files/embed.html" width="600" height="450" style="border:0;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade">
</iframe>
</div>
</div>
</div>
</div>
</section>
@@ -1,41 +1,727 @@
<div class="content-top">
<div class="lc--layout-container lc--full">
<div class="l--layout l--full">
<div class="lr--layout-region lr--main">
<div class="cc--component-container cc--homepage-hero ">
<div class="c--component c--homepage-hero">
<div class="slides-container">
<div class="image-video-container has-video">
<img src="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg" alt="Students collaborate using a driving simulator in the UMass Center for Transportation." srcset="https://www.umass.edu/sites/default/files/styles/1_1_1920x1920/public/2025-09/250616_UMASS_4515.jpg?h=9855f42d&amp;itok=d27r8GaT 1920w">
<div class="f--ambient-video" style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden; pointer-events: none; z-index: 1;">
<iframe src="https://www.youtube.com/embed/2dC8bjTpjKk?autoplay=1&mute=1&controls=0&playsinline=1&rel=0&modestbranding=1&showinfo=0" style="position: absolute; top: 50%; left: 50%; width: 100vw; height: 56.25vw; min-height: 100vh; min-width: 177.77vh; transform: translate(-50%, -50%); border: none;" allow="autoplay; encrypted-media" allowfullscreen></iframe>
</div>
<!-- <div class="video-controls" data-once="ambientVideo">
<div class="video-controls-inner">
<button aria-labelledby="pauseBtn" class="video-button video-pause-button">
...
</button>
</div>
</div> -->
</div>
<div class="text-container centered">
<div class="slide-text-container-inner">
<h3>
<a href="https://www.umass.edu/gateway/why-umass">Stroke International School 2026</a>
</h3>
<div class="f--field f--button">
<a href="https://www.umass.edu/gateway/why-umass" class="button-secondary button-context-dark "
aria-label="Learn More Learn more about UMass Amherst." data-component-id="umass_base:button">
<span class="button-text"> Learn More </span>
</a>
</div>
[menu:INFO:dropdown]
</div>
</div>
</div>
</div>
</div>
<style>
.charity-hero {
--red: #d7193f;
--red-dark: #a90f2d;
--red-soft: #fff0f4;
--pink: #ffe3ec;
--ink: #36363a;
--muted: #74747a;
--paper: #f7f7f8;
--white: #ffffff;
--shadow: 0 24px 70px rgba(174, 23, 55, .18);
--shadow-strong: 0 32px 90px rgba(100, 10, 30, .28);
}
.charity-hero {
font-family: "Segoe UI", Arial, sans-serif;
color: var(--ink);
background: #f1f1f2;
position: relative;
min-height: 546px;
overflow: hidden;
isolation: isolate;
background:
radial-gradient(circle at 72% 22%, rgba(255,255,255,.92), transparent 28%),
linear-gradient(110deg, #ffeaf0 0%, #f4f4f5 42%, #f9f9fa 100%);
}
.charity-hero * {
box-sizing: border-box;
}
.charity-hero a {
text-decoration: none;
color: inherit;
}
.charity-hero .hero-bg {
position: absolute;
inset: 0;
z-index: -2;
overflow: hidden;
}
.charity-hero .blob {
position: absolute;
border-radius: 999px;
filter: blur(3px);
opacity: .72;
animation: blobFloat 13s ease-in-out infinite alternate;
}
.charity-hero .blob-1 {
width: 460px;
height: 460px;
background: rgba(255,207,221,.75);
left: -150px;
top: -170px;
}
.charity-hero .blob-2 {
width: 520px;
height: 520px;
background: rgba(255,236,241,.9);
right: -180px;
bottom: -220px;
animation-delay: -3s;
}
.charity-hero .blob-3 {
width: 220px;
height: 220px;
background: rgba(215,25,63,.12);
left: 48%;
top: 48%;
animation-delay: -6s;
}
.charity-hero .soft-grid {
position: absolute;
inset: 22px 150px;
border: 5px solid rgba(255,206,218,.68);
border-radius: 6px;
mask: linear-gradient(90deg,transparent,black 4%,black 96%,transparent);
-webkit-mask: linear-gradient(90deg,transparent,black 4%,black 96%,transparent);
opacity: .76;
}
.charity-hero .soft-grid:before,
.charity-hero .soft-grid:after {
content: "";
position: absolute;
background: var(--red);
border-radius: 50% 50% 48% 48%;
transform: rotate(45deg);
opacity: .9;
}
.charity-hero .soft-grid:before {
width: 36px;
height: 36px;
left: 22px;
bottom: -13px;
box-shadow: 34px -1px 0 var(--red);
}
.charity-hero .soft-grid:after {
width: 17px;
height: 17px;
right: 18px;
top: 285px;
opacity: .55;
box-shadow: 20px 19px 0 rgba(215,25,63,.9);
}
.charity-hero .hero-shell {
width: min(1580px, calc(100% - 44px));
min-height: 546px;
margin: 0 auto;
display: grid;
grid-template-columns: minmax(640px, 1.1fr) minmax(470px, .9fr);
gap: 42px;
align-items: center;
padding: 36px 0;
}
.charity-hero .gallery-panel {
position: relative;
min-height: 500px;
padding: 18px 12px 0;
height: 100%;
}
.charity-hero .gallery-title {
position: absolute;
left: 22px;
top: 10px;
z-index: 22;
display: inline-flex;
flex-direction: column;
gap: 4px;
padding: 10px 16px;
border-radius: 18px;
background: rgba(255,255,255,.72);
box-shadow: 0 12px 30px rgba(70,40,50,.08);
backdrop-filter: blur(10px);
transform: translateY(-12px);
opacity: 0;
animation: fadeDown .7s ease forwards .25s;
}
.charity-hero .gallery-title .eyebrow {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 2px;
color: var(--red);
font-weight: 800;
}
.charity-hero .gallery-title strong {
font-size: 16px;
color: #313136;
}
.charity-hero .gallery-stage {
position: relative;
height: 100%;
width: 100%;
overflow: hidden;
perspective: 1200px;
}
.charity-hero .gallery-light {
position: absolute;
left: 34%;
top: 35%;
width: 310px;
height: 210px;
border-radius: 50%;
background: radial-gradient(circle, rgba(255,255,255,.88), rgba(255,255,255,0) 66%);
pointer-events: none;
opacity: .45;
mix-blend-mode: screen;
animation: lightPulse 4s ease-in-out infinite;
}
.charity-hero .focus-frame {
position: absolute;
left: 50%;
top: 50%;
width: 402px;
height: 238px;
transform: translate(-50%,-50%) scale(.86);
border-radius: 22px;
border: 1px solid rgba(215,25,63,.25);
box-shadow: 0 0 0 10px rgba(255,255,255,.16), inset 0 0 42px rgba(215,25,63,.07);
opacity: 0;
transition: .7s ease;
pointer-events: none;
z-index: 4;
}
.charity-hero .gallery-stage.is-playing .focus-frame {
opacity: 1;
transform: translate(-50%,-50%) scale(1);
}
.charity-hero .photo-card {
position: absolute;
left: var(--x);
top: var(--y);
width: var(--w);
background: #fff;
border: 4px solid var(--red);
border-radius: 3px;
overflow: hidden;
box-shadow: 0 14px 36px rgba(61,26,34,.22);
transform: translate3d(0,0,0) rotate(var(--r)) scale(.92);
opacity: 0;
z-index: 5;
will-change: left, top, width, transform, opacity, filter;
animation: cardIntro .85s cubic-bezier(.2,.9,.25,1.12) forwards;
animation-delay: var(--delay);
transition:
opacity 2.5s ease-in-out,
filter 2.5s ease-in-out,
box-shadow 1.5s cubic-bezier(.2,.8,.2,1),
border-color 1.5s cubic-bezier(.2,.8,.2,1),
left 1.5s cubic-bezier(.2,.8,.2,1),
top 1.5s cubic-bezier(.2,.8,.2,1),
width 1.5s cubic-bezier(.2,.8,.2,1),
transform 1.5s cubic-bezier(.2,.8,.2,1);
}
.charity-hero .photo-card img {
display: block;
width: 100%;
height: auto;
transform: scale(1.035);
transition: transform 1s cubic-bezier(.18,.82,.18,1), filter .8s ease;
}
.charity-hero .photo-card:before {
content: "";
position: absolute;
inset: 0;
background: linear-gradient(120deg, transparent 20%, rgba(255,255,255,.34) 45%, transparent 70%);
transform: translateX(-130%);
z-index: 2;
pointer-events: none;
}
.charity-hero .photo-card.is-active:before {
animation: shine 1.3s ease .25s;
}
.charity-hero .photo-card.is-active {
left: 50%;
top: 50%;
width: min(480px, 58vw);
transform: translate(-50%,-50%) rotate(0deg) scale(1.06) !important;
opacity: 1;
z-index: 30;
border-color: #fff;
border-width: 8px;
border-radius: 18px;
box-shadow: var(--shadow-strong);
filter: saturate(1.08) contrast(1.03);
}
.charity-hero .photo-card.is-active img {
transform: scale(1.08);
}
.charity-hero .photo-card.is-dim {
opacity: .28;
filter: grayscale(.08) saturate(.8) blur(.2px);
transform: translate3d(0,0,0) rotate(var(--r)) scale(.82);
z-index: 2;
}
.charity-hero .photo-card.is-next {
opacity: .74;
box-shadow: 0 17px 45px rgba(92,24,42,.18);
}
.charity-hero .photo-card.is-exit {
opacity: 0;
transform: translateY(28px) scale(.74) rotate(var(--r));
}
.charity-hero .gallery-controls {
position: absolute;
left: 50%;
bottom: 2px;
transform: translateX(-50%);
display: flex;
gap: 8px;
z-index: 40;
}
.charity-hero .dot {
width: 7px;
height: 7px;
border-radius: 999px;
background: rgba(215,25,63,.22);
transition: .35s ease;
cursor: pointer;
}
.charity-hero .dot.is-active {
width: 28px;
background: var(--red);
box-shadow: 0 0 0 6px rgba(215,25,63,.09);
}
.charity-hero .content-panel {
position: relative;
min-height: 430px;
display: flex;
flex-direction: column;
justify-content: center;
padding: 26px 24px 26px 8px;
}
.charity-hero .content-panel:before {
content: "";
position: absolute;
inset: 28px 0 18px 0;
border-radius: 34px;
background: linear-gradient(145deg, rgba(255,255,255,.54), rgba(255,255,255,.08));
border: 1px solid rgba(255,255,255,.55);
box-shadow: 0 20px 70px rgba(62,45,54,.06);
z-index: -1;
}
.charity-hero .brand-row {
display: grid;
grid-template-columns: 1fr 170px;
gap: 24px;
align-items: start;
margin-bottom: 54px;
}
.charity-hero .logo-mark {
width: 165px;
justify-self: end;
filter: drop-shadow(0 10px 16px rgba(215,25,63,.15));
animation: logoBreath 3.6s ease-in-out infinite;
}
.charity-hero .brand-text {
text-align: center;
align-self: center;
}
.charity-hero .brand-text h1 {
margin: 0 0 8px;
text-transform: uppercase;
line-height: 1.18;
font-size: clamp(20px,1.72vw,30px);
letter-spacing: -.3px;
color: var(--red);
font-weight: 900;
}
.charity-hero .brand-text p {
margin: 0;
color: #67676c;
text-transform: uppercase;
font-size: clamp(16px,1.2vw,22px);
font-weight: 800;
letter-spacing: .3px;
}
.charity-hero .slogan-card {
width: min(560px,100%);
margin: 0 auto;
position: relative;
text-align: center;
padding: 20px 18px 28px;
}
.charity-hero .slogan-card:before {
content: "";
position: absolute;
left: 50%;
top: 50%;
width: 86%;
height: 86%;
transform: translate(-50%,-50%);
border-radius: 38px;
background: linear-gradient(90deg,rgba(255,255,255,.18),rgba(255,237,243,.72),rgba(255,255,255,.12));
filter: blur(.2px);
z-index: -1;
opacity: .8;
}
.charity-hero .quote-mark {
position: absolute;
left: 12px;
top: -16px;
font-size: 74px;
line-height: 1;
color: rgba(215,25,63,.18);
font-family: Georgia,serif;
}
.charity-hero .slogan-card h2 {
margin: 0;
color: #e13b5e;
font-size: clamp(29px,2.2vw,42px);
line-height: 1.42;
font-family: "Segoe Script","Brush Script MT",cursive;
font-weight: 600;
text-shadow: 0 8px 20px rgba(215,25,63,.12);
}
.charity-hero .slogan-card h2 em {
font-style: normal;
font-size: .86em;
color: #d9284c;
}
.charity-hero .heart-line {
display: block;
width: 160px;
height: 22px;
margin: 12px auto 0;
position: relative;
}
.charity-hero .heart-line:before {
content: "";
position: absolute;
left: 0;
right: 34px;
top: 11px;
height: 2px;
background: linear-gradient(90deg,transparent,var(--red));
border-radius: 999px;
}
.charity-hero .heart-line:after {
content: "♡";
position: absolute;
right: 6px;
top: -8px;
color: var(--red);
font-size: 32px;
transform: rotate(18deg);
animation: heartBeat 1.7s ease-in-out infinite;
}
.charity-hero .action-row {
display: flex;
gap: 12px;
justify-content: center;
margin-top: 26px;
flex-wrap: wrap;
}
.charity-hero .btn {
display: inline-flex;
align-items: center;
justify-content: center;
height: 44px;
padding: 0 20px;
border-radius: 999px;
font-weight: 800;
font-size: 14px;
letter-spacing: .2px;
transition: .3s ease;
box-shadow: 0 12px 30px rgba(115,21,43,.12);
}
.charity-hero .btn.primary {
background: linear-gradient(135deg,var(--red),#ef4769);
color: white;
}
.charity-hero .btn.secondary {
background: rgba(255,255,255,.74);
color: var(--red);
border: 1px solid rgba(215,25,63,.18);
}
.charity-hero .reveal-up {
opacity: 0;
transform: translateY(24px);
animation: revealUp .8s ease forwards;
}
.charity-hero .brand-text.reveal-up {
animation-delay: .2s;
}
.charity-hero .logo-mark.reveal-up {
animation-delay: .38s;
}
.charity-hero .slogan-card.reveal-up {
animation-delay: .62s;
}
.charity-hero .action-row.reveal-up {
animation-delay: .86s;
}
@keyframes cardIntro {
from { opacity: 0; transform: translate3d(-28px,22px,0) rotate(calc(var(--r) - 7deg)) scale(.78); }
to { opacity: 1; transform: translate3d(0,0,0) rotate(var(--r)) scale(.92); }
}
/* Removed microFloat because animating margin-top constantly recalculates layout causing major stutter */
@keyframes shine {
to { transform: translateX(130%); }
}
@keyframes revealUp {
to { opacity: 1; transform: translateY(0); }
}
@keyframes fadeDown {
to { opacity: 1; transform: translateY(0); }
}
@keyframes logoBreath {
0%,100% { transform: translateY(0) scale(1); }
50% { transform: translateY(-6px) scale(1.025); }
}
@keyframes heartBeat {
0%,100% { transform: rotate(18deg) scale(1); }
20% { transform: rotate(18deg) scale(1.18); }
38% { transform: rotate(18deg) scale(1); }
}
@keyframes blobFloat {
to { transform: translate(26px,20px) scale(1.05); }
}
@keyframes lightPulse {
0%,100% { opacity: .25; transform: scale(.96); }
50% { opacity: .58; transform: scale(1.08); }
}
@media (max-width:1200px) {
.charity-hero .hero-shell {
grid-template-columns: 1fr;
gap: 10px;
padding: 18px 0 34px;
}
.charity-hero { min-height: auto; }
.charity-hero .content-panel { min-height: auto; }
.charity-hero .gallery-panel { min-height: 470px; }
.charity-hero .gallery-stage { height: 430px; }
.charity-hero .brand-row { grid-template-columns: 1fr 130px; margin-bottom: 20px; }
.charity-hero .logo-mark { width: 130px; }
}
@media (max-width:720px) {
.charity-hero .hero-shell { width: calc(100% - 24px); }
.charity-hero .soft-grid { inset: 12px; border-width: 3px; }
.charity-hero .gallery-panel { padding: 12px 0; min-height: 390px; }
.charity-hero .gallery-title { display: none; }
.charity-hero .gallery-stage { height: 370px; overflow: hidden; }
.charity-hero .photo-card { width: calc(var(--w) * .72); }
.charity-hero .photo-card.is-active { width: min(86vw,380px); }
.charity-hero .content-panel { padding: 8px; }
.charity-hero .brand-row { display: flex; flex-direction: column-reverse; align-items: center; gap: 10px; }
.charity-hero .brand-text h1 { text-align: center; }
.charity-hero .slogan-card h2 { font-size: 29px; }
.charity-hero .btn { height: 42px; padding: 0 16px; }
}
@media (prefers-reduced-motion:reduce) {
.charity-hero *,
.charity-hero *::before,
.charity-hero *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: .01ms !important;
}
}
</style>
<div class="charity-hero" aria-label="Quỹ từ thiện bệnh nhân nghèo bị đột quỵ">
<div class="hero-bg">
<span class="blob blob-1"></span>
<span class="blob blob-2"></span>
<span class="blob blob-3"></span>
<span class="soft-grid"></span>
</div>
<section class="hero-shell">
<div class="gallery-panel" aria-label="Hình ảnh hoạt động quỹ từ thiện">
<div class="gallery-title">
<span class="eyebrow">S.I.S Charity Foundation</span>
<strong>Những khoảnh khắc sẻ chia</strong>
</div>
<div class="gallery-stage" id="galleryStage">
<article class="photo-card p1" data-index="0" style="--x: -4%; --y: 2%; --r: -2.5deg; --w: 205px; --delay: 0.05s;">
<img src="/upload/charity_v4/photo-01.jpg" alt="Trao hỗ trợ bệnh nhân 1" />
</article>
<article class="photo-card p2" data-index="1" style="--x: 18%; --y: -2%; --r: 1.6deg; --w: 298px; --delay: 0.16s;">
<img src="/upload/charity_v4/photo-02.jpg" alt="Trao hỗ trợ bệnh nhân 2" />
</article>
<article class="photo-card p3" data-index="2" style="--x: 63%; --y: 0%; --r: -1.2deg; --w: 410px; --delay: 0.27s;">
<img src="/upload/charity_v4/photo-05.jpg" alt="Trao hỗ trợ bệnh nhân 3" />
</article>
<article class="photo-card p4" data-index="3" style="--x: 0%; --y: 27%; --r: 2.2deg; --w: 245px; --delay: 0.38s">
<img src="/upload/charity_v4/photo-03.jpg" alt="Trao hỗ trợ bệnh nhân 4" />
</article>
<article class="photo-card p5" data-index="4" style="--x: 32%; --y: 32%; --r: -1.8deg; --w: 260px; --delay: 0.49s;">
<img src="/upload/charity_v4/photo-04.jpg" alt="Trao hỗ trợ bệnh nhân 5" />
</article>
<article class="photo-card p6" data-index="5" style="--x: 57%; --y: 35%; --r: 1deg; --w: 195px; --delay: 0.6s">
<img src="/upload/charity_v4/photo-06.jpg" alt="Trao hỗ trợ bệnh nhân 6" />
</article>
<article class="photo-card p7" data-index="6" style="--x: 4%; --y: 62%; --r: -1.4deg; --w: 270px; --delay: 0.71s;">
<img src="/upload/charity_v4/photo-07.jpg" alt="Trao hỗ trợ bệnh nhân 7" />
</article>
<article class="photo-card p8" data-index="7" style="--x: 42%; --y: 73%; --r: 1.2deg; --w: 255px; --delay: 0.82s;">
<img src="/upload/charity_v4/photo-08.jpg" alt="Trao hỗ trợ bệnh nhân 8" />
</article>
<article class="photo-card p9" data-index="8" style="--x: 77%; --y: 66%; --r: -1deg; --w: 338px; --delay: 0.93s">
<img src="/upload/charity_v4/photo-09.jpg" alt="Trao hỗ trợ bệnh nhân 9" />
</article>
<div class="gallery-light" aria-hidden="true"></div>
</div>
<div class="gallery-controls" aria-hidden="true">
<span class="dot is-active"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
<span class="dot"></span>
</div>
</div>
</div>
</div>
<div class="content-panel">
<div class="brand-row">
<div class="brand-text reveal-up">
<h1>Quỹ từ thiện bệnh nhân nghèo bị đột quỵ<br />khu vực Đồng bằng sông Cửu Long</h1>
<p>S.I.S Charity Foundation</p>
</div>
<img class="logo-mark reveal-up" src="/upload/charity_v4/logo.png" alt="Stroke International Services Charity Foundation" />
</div>
<div class="slogan-card reveal-up">
<span class="quote-mark">“</span>
<h2>Sẻ chia yêu thương<br /><em>đến bệnh nhân nghèo bị đột quỵ</em></h2>
<span class="heart-line" aria-hidden="true"></span>
</div>
<div class="action-row reveal-up">
<a class="btn primary" href="#">Lan tỏa yêu thương</a>
<a class="btn secondary" href="#">Xem hoạt động quỹ</a>
</div>
</div>
</section>
</div>
<script>
(function(){
const container = document.querySelector('.charity-hero');
if(!container) return;
const stage = container.querySelector('.gallery-stage');
const cards = Array.from(container.querySelectorAll('.photo-card'));
const dots = Array.from(container.querySelectorAll('.dot'));
if(!stage || cards.length === 0) return;
let active = 0;
let timer = null;
const duration = 4500; /* Tăng thời gian chờ (autoplay) từ 2.8 giây lên 4.5 giây để nhìn rõ ảnh hơn */
function setActive(index){
active = index % cards.length;
stage.classList.add('is-playing');
cards.forEach((card, i) => {
card.classList.remove('is-active','is-dim','is-next','is-exit');
const diff = (i - active + cards.length) % cards.length;
if(i === active){
card.classList.add('is-active');
}else if(diff === 1 || diff === 2){
card.classList.add('is-next');
}else if(diff === cards.length - 1){
card.classList.add('is-exit');
}else{
card.classList.add('is-dim');
}
});
dots.forEach((dot, i) => dot.classList.toggle('is-active', i === active));
}
function next(){
setActive(active + 1);
}
function start(){
stop();
timer = setInterval(next, duration);
}
function stop(){
if(timer) clearInterval(timer);
}
// Hệ thống sẽ chạy tự động 100%
dots.forEach((dot, i) => {
dot.addEventListener('click', () => {
setActive(i);
start();
});
});
// Start immediately or on load if part of a bigger page
setTimeout(() => {
setActive(0);
start();
}, 900);
})();
</script>
@@ -0,0 +1,46 @@
package com.sisvietnamvn.web.config;
import com.sisvietnamvn.web.domain.Page;
import com.sisvietnamvn.web.domain.PageLayout;
import com.sisvietnamvn.web.domain.PageStatus;
import com.sisvietnamvn.web.domain.PageType;
import com.sisvietnamvn.web.service.PageService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;
import java.util.Optional;
@Component
public class SeedRunner implements CommandLineRunner {
private static final Logger LOG = LoggerFactory.getLogger(SeedRunner.class);
private final PageService pageService;
public SeedRunner(PageService pageService) {
this.pageService = pageService;
}
@Override
public void run(String... args) {
try {
Optional<Page> existingPage = pageService.findByPageType(PageType.CONTACT_US);
Page page = existingPage.orElseGet(Page::new);
LOG.info("Seeding/Updating Contact Us Page...");
page.setTitle("Liên hệ");
page.setSlug("contact-us");
page.setPageType(PageType.CONTACT_US);
page.setStatus(PageStatus.PUBLISHED);
page.setLayout(PageLayout.STANDARD);
String content = "{\"time\":1721470000000,\"blocks\":[{\"type\":\"header\",\"data\":{\"text\":\"Cơ sở 1\",\"level\":2}},{\"type\":\"list\",\"data\":{\"style\":\"unordered\",\"items\":[\"Địa chỉ: 215 Hồng Bàng, Phường 11, Quận 5, TP.HCM\",\"Điện thoại: (84.28) 3855 4269\",\"Email: bvdhyd@umc.edu.vn\",\"Website: https://bvdaihoc.com.vn\",\"Map: https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3919.646549247659!2d106.66014561533418!3d10.75539506240683!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x31752eef2f3484f9%3A0xc34ccfa994f1388b!2sUniversity%20Medical%20Center%20HCMC!5e0!3m2!1sen!2s!4v1689304381861!5m2!1sen!2s\"]}},{\"type\":\"header\",\"data\":{\"text\":\"Cơ sở 2\",\"level\":2}},{\"type\":\"list\",\"data\":{\"style\":\"unordered\",\"items\":[\"Địa chỉ: 201 Nguyễn Chí Thanh, Phường 12, Quận 5, TP.HCM\",\"Điện thoại: (84.28) 3955 5548\",\"Email: bvdaihoccoso2@umc.edu.vn\",\"Website: https://bvdaihoccoso2.com.vn\",\"Map: https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3919.6105822604626!2d106.66107411533414!3d10.764472862358826!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x31752efc8f2ba129%3A0x63353bd7e2dbf9e!2zQ8ahIHPhu58gMiBC4buHbmggdmnhu4duIMSQ4bqhaSBo4buNYyBZIETGsOG7o2MgVFAuSENN!5e0!3m2!1sen!2s!4v1689304419810!5m2!1sen!2s\"]}},{\"type\":\"header\",\"data\":{\"text\":\"Cơ sở 3\",\"level\":2}},{\"type\":\"list\",\"data\":{\"style\":\"unordered\",\"items\":[\"Địa chỉ: 221B Hoàng Văn Thụ, Phường 8, Quận Phú Nhuận, TP.HCM\",\"Điện thoại: (84.28) 3842 0070\",\"Email: bvdaihoccoso3@umc.edu.vn\",\"Website: https://bvdaihoccoso3.com.vn\",\"Map: https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3919.1673832863925!2d106.67498701533446!3d10.801646261739345!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x317528dfeb485ec3%3A0x8898124976ea6558!2zQ8ahIHPhu58gMyBC4buHbmggdmnhu4duIMSQ4bqhaSBo4buNYyBZIETGsOG7o2MgVFAuSENN!5e0!3m2!1sen!2s!4v1689304443657!5m2!1sen!2s\"]}},{\"type\":\"header\",\"data\":{\"text\":\"Đơn vị hợp tác\",\"level\":2}},{\"type\":\"list\",\"data\":{\"style\":\"unordered\",\"items\":[\"Địa chỉ: 20-22 Dương Quang Trung, Phường Hòa Hưng, Quận 10, TP.HCM\",\"Điện thoại: 1900 6923\",\"Email: contact.us@umcclinic.com.vn\",\"Website: https://umcclinic.com.vn/\",\"Map: https://www.google.com/maps/embed?pb=!1m18!1m12!1m3!1d3919.4244583196924!2d106.66699101533423!3d10.778768062145718!2m3!1f0!2f0!3f0!3m2!1i1024!2i768!4f13.1!3m3!1m2!1s0x31752ed1807ebf05%3A0xc3f8e53be131ccb4!2sPh%C3%B2ng%20kh%C3%A1m%20B%E1%BB%87nh%20vi%E1%BB%87n%20%C4%90%E1%BA%A1i%20h%E1%BB%8Dc%20Y%20D%C6%B0%E1%BB%A3c%201!5e0!3m2!1sen!2s!4v1689304495719!5m2!1sen!2s\"]}}],\"version\":\"2.29.1\"}";
page.setContent(content);
pageService.save(page);
LOG.info("Contact Us Page seeded successfully!");
} catch (Exception e) {
LOG.error("Failed to seed Contact Us Page", e);
}
}
}
@@ -137,6 +137,11 @@ public class PageController {
model.addAttribute("page", page);
model.addAttribute("blocks", blocks);
if (com.sisvietnamvn.web.domain.PageType.CONTACT_US.equals(page.getPageType())) {
return "pages/contact-us";
}
return "page";
}
}
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-latest.xsd">
<changeSet id="20260720181500-1" author="antigravity" context="dev, faker">
<!-- Delete if exists to avoid unique constraint on slug -->
<delete tableName="sis_page">
<where>slug = 'contact-us'</where>
</delete>
<insert tableName="sis_page">
<column name="id" valueSequenceNext="sequenceGenerator"/>
<column name="title" value="Contact Us"/>
<column name="slug" value="contact-us"/>
<column name="content" value='{"time": 1721470000000,"blocks":[{"type":"header","data":{"text":"General Information","level":2}},{"type":"paragraph","data":{"text":"For General Campus Inquiries, please call the main switchboard at 413-545-0111. Operators are available from 8:00 a.m. to 5:00 p.m., Monday through Friday."}},{"type":"header","data":{"text":"Directories","level":2}},{"type":"paragraph","data":{"text":"Find contact information for academic departments, campus services, and other resources."}}],"version":"2.29.1"}'/>
<column name="status" value="PUBLISHED"/>
<column name="page_type" value="CONTACT_US"/>
<column name="layout" value="STANDARD"/>
<column name="created_by" value="system"/>
<column name="created_date" valueDate="2026-07-20T00:00:00"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -41,4 +41,5 @@
<include file="config/liquibase/changelog/20260714113000_update_events_list.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260718100000_add_doctor_and_specialty.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260718110000_seed_sample_doctors.xml" relativeToChangelogFile="false"/>
<include file="config/liquibase/changelog/20260720181500_seed_contact_us_page.xml" relativeToChangelogFile="false"/>
</databaseChangeLog>
File diff suppressed because one or more lines are too long
@@ -0,0 +1,207 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
layout:decorate="~{fragments/layout}">
<head>
<title th:text="${page.title} + ' | S.I.S Cần Thơ'">Liên hệ | S.I.S Cần Thơ</title>
<link rel="stylesheet" th:href="@{/css/bvdhyd.css}">
</head>
<body>
<div layout:fragment="content" id="bvdhyd-contact">
<!-- Hero Section -->
<section class="overflow-hidden relative w-full bg-white xl:h-[580px] md:h-[550px] h-[calc(100svh-122px-56px)]">
<div class="bg-linear-hero absolute inset-x-0 bottom-0 top-[20%] z-10"></div>
<img alt="Bệnh viện" fetchpriority="high" loading="eager" class="absolute inset-0 w-full h-full object-cover max-md:hidden" src="https://bvdaihoc.com.vn/uploads/Demo/to%CC%80a%20nha%CC%80%20tru%CC%9Bo%CC%9B%CC%80ng%20vie%CC%A3%CC%82n%20la%CC%80m%20mo%CC%9B%CC%80.jpg" />
</section>
<!-- Main Content -->
<section class="xl:py-16 md:py-12 py-8 md:scroll-mt-[186px] relative z-20" id="map-section">
<div class="container mx-auto px-4 lg:px-10 space-y-4 md:space-y-6 xl:space-y-8 max-w-7xl">
<h1 th:text="${page.title}" class="text-primary-600 display-7 text-center uppercase">Liên hệ</h1>
<div class="grid grid-cols-12 gap-6">
<!-- Left Sidebar (Branches) -->
<div class="col-span-full lg:col-span-5 xl:col-span-4 rounded-lg pr-2 pl-4 py-4 bg-gray-100">
<div id="branch-list" class="space-y-4 overflow-y-auto h-[500px] pr-2 custom-scrollbar">
<!-- Cards will be dynamically injected here by JS -->
</div>
</div>
<!-- Right Main Map -->
<div class="col-span-full lg:col-span-7 xl:col-span-8">
<div class="relative rounded-lg overview-wrapper-shadow overflow-hidden w-full h-full max-lg:h-[450px] [&>iframe]:w-full [&>iframe]:h-full [&>iframe]:border-0">
<iframe id="map-iframe" src="" width="100%" height="100%" style="border:0; min-height:450px;" allowfullscreen="" loading="lazy" referrerpolicy="no-referrer-when-downgrade"></iframe>
</div>
</div>
</div>
</div>
</section>
<!-- Hidden data -->
<div id="postContentRaw" style="display:none;" th:text="${page.content}"></div>
<script>
document.addEventListener("DOMContentLoaded", function() {
const rawData = document.getElementById("postContentRaw").textContent;
const branchListContainer = document.getElementById("branch-list");
const mapIframe = document.getElementById("map-iframe");
try {
const data = JSON.parse(rawData);
let branches = [];
let currentBranch = null;
if (data.blocks) {
data.blocks.forEach(block => {
if (block.type === "header" && block.data.level === 2) {
if (currentBranch) {
branches.push(currentBranch);
}
currentBranch = {
name: block.data.text,
address: '',
phone: '',
email: '',
website: '',
mapUrl: ''
};
} else if (block.type === "list" && currentBranch) {
block.data.items.forEach(item => {
// Remove html tags if any are embedded in string
const tempDiv = document.createElement("div");
tempDiv.innerHTML = item;
const text = tempDiv.textContent || tempDiv.innerText || "";
if (text.toLowerCase().startsWith('địa chỉ')) currentBranch.address = text.split(':').slice(1).join(':').trim();
else if (text.toLowerCase().startsWith('điện thoại')) currentBranch.phone = text.split(':').slice(1).join(':').trim();
else if (text.toLowerCase().startsWith('email')) currentBranch.email = text.split(':').slice(1).join(':').trim();
else if (text.toLowerCase().startsWith('website')) currentBranch.website = text.split(':').slice(1).join(':').trim();
else if (text.toLowerCase().startsWith('map')) currentBranch.mapUrl = text.split(':').slice(1).join(':').trim();
});
}
});
if (currentBranch) branches.push(currentBranch);
}
if (branches.length > 0) {
let html = '';
branches.forEach((branch, index) => {
const isFirst = index === 0;
// Set initial iframe src
if (isFirst && branch.mapUrl) {
mapIframe.src = branch.mapUrl;
}
const bgClass = isFirst ? 'bg-primary-600' : 'bg-white';
const titleClass = isFirst ? 'text-white' : 'text-primary-600';
const iconClass = isFirst ? 'text-white' : 'text-primary-600';
const textClass = isFirst ? 'text-white' : 'text-gray-900';
html += `
<div class="branch-card rounded-lg overflow-hidden cursor-pointer ${bgClass}" data-map-url="${branch.mapUrl}" onclick="selectBranch(this)">
<div class="xl:p-4 p-3 space-y-2">
<div class="title-1 ${titleClass} branch-title">${branch.name}</div>
${branch.address ? `
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-map-pin size-4 flex-shrink-0 mt-1 branch-icon ${iconClass}"><path d="M20 10c0 4.993-5.539 10.193-7.399 11.799a1 1 0 0 1-1.202 0C9.539 20.193 4 14.993 4 10a8 8 0 0 1 16 0"/><circle cx="12" cy="10" r="3"/></svg>
<a href="${branch.mapUrl}" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:duration-150 lg:hover:underline underline-offset-4 branch-text ${textClass}">${branch.address}</a>
</div>` : ''}
${branch.phone ? `
<div class="flex items-start gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-phone size-4 flex-shrink-0 mt-1 branch-icon ${iconClass}"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z"/></svg>
<a href="tel:${branch.phone.replace(/[^0-9]/g, '')}" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 branch-text ${textClass}">${branch.phone}</a>
</div>` : ''}
${branch.email ? `
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-mail size-4 flex-shrink-0 branch-icon ${iconClass}"><rect width="20" height="16" x="2" y="4" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>
<a href="mailto:${branch.email}" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 branch-text ${textClass}">${branch.email}</a>
</div>` : ''}
${branch.website ? `
<div class="flex items-center gap-2">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-globe size-4 flex-shrink-0 branch-icon ${iconClass}"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>
<a href="${branch.website}" target="_blank" rel="noopener noreferrer nofollow" class="flex-1 body-2 lg:hover:underline underline-offset-4 lg:duration-150 branch-text ${textClass}">${branch.website}</a>
</div>` : ''}
</div>
</div>
`;
});
branchListContainer.innerHTML = html;
}
} catch(e) {
console.error("Error parsing contact data", e);
}
});
// Global function to handle branch selection
function selectBranch(element) {
// Update active state of all cards
const cards = document.querySelectorAll('.branch-card');
cards.forEach(card => {
// Reset to inactive state
card.className = 'branch-card rounded-lg overflow-hidden cursor-pointer bg-white';
// Update text colors
const title = card.querySelector('.branch-title');
if (title) title.className = 'title-1 text-primary-600 branch-title';
const icons = card.querySelectorAll('.branch-icon');
icons.forEach(icon => {
icon.classList.remove('text-white');
icon.classList.add('text-primary-600');
});
const texts = card.querySelectorAll('.branch-text');
texts.forEach(text => {
text.classList.remove('text-white');
text.classList.add('text-gray-900');
});
});
// Set active state on clicked card
element.className = 'branch-card rounded-lg overflow-hidden cursor-pointer bg-primary-600';
const title = element.querySelector('.branch-title');
if (title) title.className = 'title-1 text-white branch-title';
const icons = element.querySelectorAll('.branch-icon');
icons.forEach(icon => {
icon.classList.remove('text-primary-600');
icon.classList.add('text-white');
});
const texts = element.querySelectorAll('.branch-text');
texts.forEach(text => {
text.classList.remove('text-gray-900');
text.classList.add('text-white');
});
// Update Map URL
const mapUrl = element.getAttribute('data-map-url');
const mapIframe = document.getElementById("map-iframe");
if (mapUrl && mapIframe) {
mapIframe.src = mapUrl;
}
}
</script>
<style>
/* Custom scrollbar for branch list */
.custom-scrollbar::-webkit-scrollbar {
width: 6px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
.custom-scrollbar::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
</style>
</div>
</body>
</html>