feat: implement Contact Us page with dynamic database seeding and dedicated view controller logic
This commit is contained in:
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
+25
@@ -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>
|
||||
Reference in New Issue
Block a user