You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1275 lines
34 KiB

6 days ago
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
​
  1. <!DOCTYPE html>
  2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
  3. <head th:replace="~{fragments/user-head :: head}"></head>
  4. <style>
  5. :root {
  6. --bg: #f6f3ef;
  7. --accent: #12363b;
  8. --dark: #2f1f1b;
  9. --muted: #7b7773;
  10. --panel-w: 660px;
  11. --radius: 14px;
  12. }
  13. /* Reset */
  14. * {
  15. box-sizing: border-box;
  16. margin: 0;
  17. padding: 0
  18. }
  19. html,
  20. body {
  21. height: 100%
  22. }
  23. body {
  24. font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
  25. background:
  26. linear-gradient(rgba(255, 255, 255, 0.70), rgba(255, 255, 255, 0.82)),
  27. url('/victoriapublichall/images/vph-background_v1.jpg') top center / cover no-repeat,
  28. url('/victoriapublichall/images/vph-background_v1.jpg') bottom center / cover no-repeat;
  29. min-height: 100vh;
  30. background-attachment: fixed;
  31. /* keeps both images static */
  32. background-repeat: no-repeat;
  33. background-size: cover;
  34. -webkit-font-smoothing: antialiased;
  35. min-height: 100vh;
  36. display: flex;
  37. flex-direction: column;
  38. gap: 12px;
  39. padding: 18px 24px;
  40. }
  41. /* Mobile view image should be FULL COVER, NO REPEAT */
  42. @media (max-width: 520px) {
  43. body {
  44. background:
  45. linear-gradient(rgba(255, 255, 255, 0.75), rgba(255, 255, 255, 0.90)),
  46. url('/victoriapublichall/images/vph-background_v1.jpg');
  47. background-size: 100em;
  48. /* full cover */
  49. background-repeat: no-repeat;
  50. background-position: center center;
  51. background-attachment: scroll;
  52. /* prevents weird zooming on mobile */
  53. /* remove second image on mobile */
  54. }
  55. }
  56. /* container */
  57. .container {
  58. width: 100%;
  59. max-width: 1200px;
  60. margin: 0 auto;
  61. }
  62. /******************************
  63. NAVBAR (desktop + mobile)
  64. ******************************/
  65. .topbar {
  66. display: flex;
  67. align-items: center;
  68. justify-content: space-between;
  69. gap: 16px;
  70. padding: 10px 0;
  71. position: relative;
  72. z-index: 1300;
  73. }
  74. .leftbar {
  75. display: flex;
  76. align-items: center;
  77. gap: 20px;
  78. }
  79. /* small logo + layout helpers */
  80. .nav-left {
  81. display: flex;
  82. align-items: center;
  83. gap: 12px;
  84. }
  85. .logo {
  86. width: 72px;
  87. height: 72px;
  88. object-fit: contain;
  89. border-radius: 50%;
  90. }
  91. /* default desktop nav */
  92. nav.primary {
  93. display: flex;
  94. gap: 18px;
  95. align-items: center;
  96. }
  97. /* Desktop Navigation Links (Figma Accurate) */
  98. nav.primary a {
  99. font-family: "Playfair Display", serif !important;
  100. font-weight: 400 !important;
  101. /* Regular */
  102. font-style: normal !important;
  103. font-size: 15px !important;
  104. /* Figma size */
  105. line-height: 100% !important;
  106. letter-spacing: 0 !important;
  107. color: #3C1E12;
  108. /* matches Book a Visit color */
  109. text-decoration: none;
  110. padding: 6px 8px;
  111. /* keep your layout spacing */
  112. border-radius: 6px;
  113. transition: background 0.2s ease;
  114. }
  115. /* Hover state (optional subtle effect like Figma preview) */
  116. nav.primary a:hover {
  117. background: rgba(0, 0, 0, 0.06);
  118. color: #3C1E12;
  119. /* no change in color */
  120. }
  121. /* Mobile menu links */
  122. .mobile-nav a {
  123. font-family: "Playfair Display", serif !important;
  124. font-weight: 400 !important;
  125. font-size: 15px !important;
  126. letter-spacing: 0 !important;
  127. color: #3C1E12;
  128. }
  129. .rightbar {
  130. display: flex;
  131. align-items: center;
  132. gap: 12px;
  133. }
  134. /* desktop CTA (Book a Visit) */
  135. /* Desktop CTA (Book a Visit) — Updated with Figma specs */
  136. /* Desktop CTA (Book a Visit) — With Figma Border */
  137. #desktopBookBtn {
  138. width: 174px;
  139. /* Figma button width */
  140. height: 57px;
  141. /* Figma button height */
  142. display: inline-flex;
  143. align-items: center;
  144. justify-content: center;
  145. background: transparent;
  146. border-radius: 8px;
  147. /* Figma corner radius */
  148. /* Border from Figma (thin outline) */
  149. border: 1px solid rgba(60, 30, 18, 0.40);
  150. /* darker visible border */
  151. cursor: pointer;
  152. /* Figma Typography */
  153. font-family: "Playfair Display", serif !important;
  154. font-weight: 700 !important;
  155. /* Bold */
  156. font-style: italic !important;
  157. font-size: 24px !important;
  158. line-height: 100% !important;
  159. /* Letter spacing -6% = -0.06em */
  160. letter-spacing: -0.06em !important;
  161. color: #3C1E12 !important;
  162. }
  163. /* Hover effect (Figma-like subtle shadow) */
  164. #desktopBookBtn:hover {
  165. box-shadow: 0 2px 6px rgba(0, 0, 0, 0.10);
  166. }
  167. /* Keep existing responsive behavior */
  168. @media (max-width: 900px) {
  169. nav.primary {
  170. display: none !important;
  171. }
  172. #desktopBookBtn {
  173. display: none !important;
  174. }
  175. .hamburger {
  176. display: flex !important;
  177. }
  178. /* Mobile logo size */
  179. .logo {
  180. width: 44px;
  181. height: 44px;
  182. }
  183. }
  184. /* hamburger (hidden on desktop) */
  185. .hamburger {
  186. display: none;
  187. align-items: center;
  188. justify-content: center;
  189. width: 44px;
  190. height: 44px;
  191. border-radius: 8px;
  192. border: 1px solid rgba(0, 0, 0, 0.06);
  193. background: transparent;
  194. cursor: pointer;
  195. padding: 8px;
  196. }
  197. .hamburger .bar {
  198. display: block;
  199. width: 18px;
  200. height: 2px;
  201. background: var(--dark);
  202. margin: 3px 0;
  203. transition: transform .2s, opacity .2s;
  204. }
  205. /* mobile floating menu + overlay (initial hidden) */
  206. .mobile-overlay {
  207. position: fixed;
  208. inset: 0;
  209. background: rgba(0, 0, 0, 0.22);
  210. z-index: 1350;
  211. opacity: 0;
  212. pointer-events: none;
  213. transition: opacity .22s ease;
  214. }
  215. .mobile-overlay.show {
  216. opacity: 1;
  217. pointer-events: auto;
  218. }
  219. .mobile-nav {
  220. position: fixed;
  221. left: 12px;
  222. right: 12px;
  223. top: 68px;
  224. max-width: 420px;
  225. margin: 0 auto;
  226. background: #fff;
  227. border-radius: 12px;
  228. padding: 14px;
  229. box-shadow: 0 22px 60px rgba(0, 0, 0, 0.16);
  230. border: 1px solid rgba(0, 0, 0, 0.06);
  231. z-index: 1400;
  232. transform: translateY(-8px) scale(.995);
  233. opacity: 0;
  234. pointer-events: none;
  235. transition: opacity .22s ease, transform .22s ease;
  236. }
  237. .mobile-nav.open {
  238. opacity: 1;
  239. transform: translateY(0) scale(1);
  240. pointer-events: auto;
  241. }
  242. .mobile-nav a {
  243. display: block;
  244. padding: 10px 12px;
  245. color: var(--dark);
  246. text-decoration: none;
  247. border-radius: 8px;
  248. font-weight: 600;
  249. }
  250. .mobile-nav a+a {
  251. margin-top: 8px;
  252. }
  253. .mobile-nav .book-cta {
  254. background: #f7f9f9;
  255. text-align: center;
  256. padding: 12px;
  257. color: var(--accent);
  258. font-weight: 700;
  259. }
  260. /* small screens tweak */
  261. @media (max-width:520px) {
  262. .logo {
  263. width: 61px !important;
  264. /* Figma width */
  265. height: 61px !important;
  266. /* Figma height */
  267. object-fit: contain;
  268. }
  269. .mobile-nav {
  270. left: 12px;
  271. right: 12px;
  272. top: 68px;
  273. max-width: 420px;
  274. margin: 0 auto;
  275. background: #fff;
  276. border-radius: 12px;
  277. padding: 14px;
  278. box-shadow: 0 22px 60px rgba(0, 0, 0, 0.16);
  279. border: 1px solid rgba(0, 0, 0, 0.06);
  280. z-index: 1400;
  281. transform: translateY(-8px) scale(.995);
  282. opacity: 0;
  283. pointer-events: none;
  284. transition: opacity .22s ease, transform .22s ease;
  285. }
  286. }
  287. /******************************
  288. PAGE / HERO / PANEL
  289. ******************************/
  290. .page {
  291. width: 100%;
  292. display: flex;
  293. flex-direction: column;
  294. align-items: center;
  295. justify-content: flex-start;
  296. margin-top: 20px;
  297. }
  298. .panel-wrap {
  299. width: 100%;
  300. display: flex;
  301. justify-content: center;
  302. align-items: center;
  303. margin-top: 18px;
  304. }
  305. .hero {
  306. width: 100%;
  307. max-width: 920px;
  308. text-align: center;
  309. margin-bottom: 12px;
  310. }
  311. /* Desktop style */
  312. .page h1 {
  313. font-family: "Inter", sans-serif;
  314. font-weight: 700;
  315. font-style: normal;
  316. font-size: 48px;
  317. line-height: 100%;
  318. letter-spacing: 0;
  319. text-align: center;
  320. width: 100%;
  321. max-width: 444px;
  322. /* Figma width */
  323. margin: 0 auto 10px;
  324. /* Center + bottom spacing */
  325. color: #3B1E14 !important;
  326. }
  327. /* Mobile view */
  328. @media (max-width:520px) {
  329. .page h1 {
  330. font-family: "Inter", sans-serif;
  331. font-weight: 700;
  332. font-style: normal;
  333. font-size: 32px !important;
  334. /* Figma size */
  335. line-height: 100% !important;
  336. /* Figma line height */
  337. letter-spacing: 0;
  338. width: 296px;
  339. /* Figma width */
  340. max-width: 90%;
  341. /* Responsive safety */
  342. margin: 0 auto;
  343. /* Center horizontally */
  344. text-align: center;
  345. /* Figma alignment */
  346. color: #3B1E14 !important;
  347. /* Requested color */
  348. }
  349. }
  350. /* Desktop version */
  351. .hero .lead {
  352. font-family: "Inter", sans-serif;
  353. font-weight: 400;
  354. font-style: normal;
  355. font-size: 20px;
  356. line-height: 100%;
  357. letter-spacing: 0;
  358. text-align: center;
  359. width: 100%;
  360. max-width: 417px;
  361. /* From Figma */
  362. margin: 0 auto 20px;
  363. /* Center + spacing */
  364. color: #000000;
  365. /* BLACK COLOR as requested */
  366. }
  367. /* Mobile version */
  368. @media (max-width:520px) {
  369. .hero .lead {
  370. font-family: "Inter", sans-serif;
  371. font-weight: 400;
  372. font-style: normal;
  373. font-size: 14px !important;
  374. /* Figma size */
  375. line-height: 100% !important;
  376. /* Figma line height */
  377. letter-spacing: 0;
  378. width: 275px;
  379. /* Figma width */
  380. max-width: 90%;
  381. /* keep responsive */
  382. margin: 0 auto 20px;
  383. /* center align */
  384. text-align: center;
  385. /* Figma alignment */
  386. color: #000000 !important;
  387. /* Text color */
  388. }
  389. }
  390. /* Desktop version */
  391. .panel h3 {
  392. font-family: "Inter", sans-serif;
  393. font-weight: 700;
  394. /* bold */
  395. font-style: normal;
  396. font-size: 20px;
  397. line-height: 100%;
  398. letter-spacing: 0;
  399. text-align: center;
  400. color: #000000;
  401. /* BLACK TEXT */
  402. width: 100%;
  403. max-width: 336px;
  404. /* Figma width */
  405. margin: 0 auto 14px;
  406. /* center + bottom spacing */
  407. }
  408. /* Mobile version */
  409. @media (max-width:520px) {
  410. .panel h3 {
  411. font-size: 24px;
  412. /* slightly smaller */
  413. max-width: 120%;
  414. line-height: 140%;
  415. }
  416. }
  417. /* Desktop version */
  418. .muted-label {
  419. font-family: "Inter", sans-serif;
  420. font-weight: 700;
  421. font-style: normal;
  422. font-size: 16px;
  423. line-height: 100%;
  424. letter-spacing: 0;
  425. color: #3B1E14;
  426. width: 100%;
  427. max-width: 100%;
  428. margin-bottom: 8px;
  429. text-align: left;
  430. display: block;
  431. margin-left: 40px !important;
  432. /* ⬅ FIXES CENTER ISSUE */
  433. }
  434. /* Mobile version */
  435. @media (max-width:520px) {
  436. .muted-label {
  437. font-size: 16px;
  438. max-width: 1400%;
  439. line-height: 140%;
  440. text-align: left;
  441. margin-left: 0 !important;
  442. /* keep left */
  443. }
  444. }
  445. /* Desktop version */
  446. .input {
  447. width: 100%;
  448. max-width: 536px;
  449. /* Figma width */
  450. padding: 16px 18px;
  451. /* creates approx 65px height */
  452. border-radius: 14px;
  453. /* Figma radius */
  454. border: 1px solid rgba(0, 0, 0, 0.06);
  455. background: #ffffff;
  456. font-family: "Inter", sans-serif;
  457. font-size: 16px;
  458. outline: none;
  459. box-sizing: border-box;
  460. }
  461. /* Focus glow */
  462. .input:focus {
  463. box-shadow: 0 8px 26px rgba(20, 30, 30, 0.04);
  464. border-color: rgba(0, 0, 0, 0.14);
  465. }
  466. /* Mobile version */
  467. @media (max-width:520px) {
  468. .input {
  469. max-width: 140%;
  470. line-height: 140%;
  471. padding: 16px 18px;
  472. /* better fit on mobile */
  473. border-radius: 12px;
  474. }
  475. }
  476. /* Desktop version */
  477. .btn {
  478. width: 100%;
  479. max-width: 536px;
  480. /* Figma width */
  481. padding: 18px 20px;
  482. /* visual height approx 66px */
  483. border-radius: 11px;
  484. /* Figma radius */
  485. background: var(--accent);
  486. /* your theme greenish color */
  487. color: #ffffff;
  488. font-family: "Inter", sans-serif;
  489. font-weight: 700;
  490. font-size: 16px;
  491. border: none;
  492. cursor: pointer;
  493. box-shadow: 0 8px 26px rgba(18, 54, 59, 0.12);
  494. transition: none !important;
  495. /* Animate: Instant */
  496. }
  497. /* Disabled version */
  498. .btn[disabled] {
  499. opacity: .6;
  500. cursor: not-allowed;
  501. box-shadow: none;
  502. }
  503. /* Mobile version */
  504. @media (max-width:520px) {
  505. .btn {
  506. max-width: 140%;
  507. line-height: 140%;
  508. padding: 16px 18px;
  509. /* smaller height on mobile */
  510. border-radius: 12px;
  511. font-size: 18px;
  512. }
  513. }
  514. .links-row {
  515. width: 100%;
  516. max-width: 536px;
  517. display: flex;
  518. justify-content: space-between;
  519. /* pushes them to opposite sides */
  520. align-items: center;
  521. margin: 8px auto 0;
  522. /* center align inside panel */
  523. padding: 0 4px;
  524. /* small spacing for balance */
  525. }
  526. /* Change phone/email */
  527. .changeLinkrow a {
  528. font-family: "Inter", sans-serif;
  529. font-size: 16px;
  530. font-weight: 400;
  531. line-height: 100%;
  532. color: #0478D0;
  533. text-decoration: underline;
  534. text-decoration-thickness: 1px;
  535. text-underline-offset: 2px;
  536. cursor: pointer;
  537. transition: none !important;
  538. }
  539. .changeLinkrow a.disabled {
  540. color: rgb(0, 128, 192) !important;
  541. pointer-events: none;
  542. }
  543. /* Resend OTP */
  544. .resendLinkrow {
  545. text-align: right;
  546. }
  547. .resendLinkrow a {
  548. font-family: "Inter", sans-serif;
  549. font-size: 16px;
  550. font-weight: 400;
  551. line-height: 100%;
  552. color: #0478D0;
  553. text-decoration: underline;
  554. text-decoration-thickness: 1px;
  555. text-underline-offset: 2px;
  556. cursor: pointer;
  557. transition: none !important;
  558. }
  559. .resendLinkrow a.disabled {
  560. color: #0478D0;
  561. pointer-events: none;
  562. }
  563. .otp-input {
  564. width: 536px;
  565. /* height: 65px; */
  566. max-width: 100%;
  567. /* keeps it responsive on mobile */
  568. padding: 16px 18px;
  569. border-radius: 14px;
  570. /* Figma: Radius 14px */
  571. background: #ffffff;
  572. border: 1px solid rgba(0, 0, 0, 0.06);
  573. font-family: "Inter", sans-serif;
  574. font-size: 20px;
  575. /* OTP input usually bigger for readability */
  576. font-weight: 400;
  577. line-height: 100%;
  578. outline: none;
  579. box-sizing: border-box;
  580. }
  581. /* Focus effect (same style as your other inputs) */
  582. .otp-input:focus {
  583. border-color: rgba(0, 0, 0, 0.12);
  584. box-shadow: 0 8px 26px rgba(20, 30, 30, 0.04);
  585. }
  586. /* Mobile View – make it fit screen */
  587. @media (max-width: 520px) {
  588. .otp-input {
  589. max-width: 140%;
  590. line-height: 140%;
  591. padding: 16px 18px;
  592. width: 100%;
  593. height: 55px;
  594. font-size: 18px;
  595. border-radius: 12px;
  596. }
  597. }
  598. .panel {
  599. width: 100%;
  600. max-width: var(--panel-w);
  601. /* GLASS EFFECT PANEL */
  602. background: rgba(255, 255, 255, 0);
  603. border-radius: var(--radius);
  604. padding: 26px;
  605. border: 1px solid rgba(255, 255, 255, 0);
  606. }
  607. </style>
  608. <body>
  609. <div class="container">
  610. <!-- ============================
  611. NAVBAR: REPLACE/EDIT HERE
  612. (Brand + desktop nav + CTA; mobile hamburger + menu below)
  613. ============================ -->
  614. <!-- NAVBAR (replace your existing header / mobile nav markup with this) -->
  615. <header class="topbar" role="banner">
  616. <div class="leftbar">
  617. <div class="nav-left">
  618. <!-- logo image — use same img src you already have -->
  619. <img th:src="@{images/VPH-logo_main-EN 1.png}" alt="VPH logo" class="logo">
  620. </div>
  621. <!-- desktop nav (hidden on mobile) -->
  622. <nav class="primary" aria-label="Primary navigation">
  623. <a href="/victoriapublichall/home">Home</a>
  624. <a href="#">About VPH</a>
  625. <a href="/victoriapublichall/visit">Visit</a>
  626. <a href="#">Contact Us</a>
  627. </nav>
  628. </div>
  629. <!-- right side: hamburger (mobile) + desktop CTA -->
  630. <div class="rightbar">
  631. <button id="hambtn" class="hamburger" aria-label="Open menu" aria-expanded="false" aria-controls="mobileNav">
  632. <span class="bar bar1" aria-hidden="true"></span>
  633. <span class="bar bar2" aria-hidden="true"></span>
  634. <span class="bar bar3" aria-hidden="true"></span>
  635. </button>
  636. </div>
  637. </header>
  638. <!-- overlay (click to close) -->
  639. <div id="mobileOverlay" class="mobile-overlay" aria-hidden="true"></div>
  640. <!-- mobile nav -->
  641. <nav id="mobileNav" class="mobile-nav" aria-hidden="true" role="dialog" aria-modal="true" aria-label="Mobile menu">
  642. <a href="/victoriapublichall/home">Home</a>
  643. <a href="#">About VPH</a>
  644. <a href="/victoriapublichall/visit">Visit</a>
  645. <a href="#">Contact Us</a>
  646. </nav>
  647. <!-- ============================
  648. PAGE CONTENT (HERO + OTP PANEL)
  649. ============================ -->
  650. <main class="page" role="main" aria-labelledby="hero-heading">
  651. <div class="hero">
  652. <h1 id="hero-heading">Victoria Public Hall</h1>
  653. <div class="lead">Experience Chennai's First Town Hall</div>
  654. <div class="panel-wrap">
  655. <div class="panel" role="region" aria-label="OTP booking panel">
  656. <h3>Book your visit / Manage Bookings</h3>
  657. <br>
  658. <span class="muted-label" for="contactInput">Enter phone number or email to proceed</span>
  659. <!-- initial input -->
  660. <div id="input-block">
  661. <input id="contactInput" class="input" type="text" inputmode="text" placeholder="Phone | Email"
  662. autocomplete="off" aria-label="Phone or Email">
  663. <div style="height:12px"></div>
  664. <button id="sendBtn" class="btn" type="button">Send OTP</button>
  665. </div>
  666. <!-- after-send block -->
  667. <div id="sent-block" style="display:none">
  668. <input id="contactInputDisabled" class="input" type="text" disabled>
  669. <div style="height:8px"></div>
  670. <button id="sendBtnDisabled" class="btn" disabled>Send OTP</button>
  671. <div class="links-row" style="margin-top:8px">
  672. <div class="changeLinkrow">
  673. <a href="#" id="changeLink">Change phone/email</a>
  674. </div>
  675. <div class="resendLinkrow">
  676. <a href="#" id="resendLink" class="disabled">Resend OTP</a>
  677. </div>
  678. </div>
  679. <div class="otp-area" id="otpArea" aria-hidden="true">
  680. <span class="muted-label" for="otpInput" style="margin-top:12px">Enter OTP</span>
  681. <input id="otpInput" class="otp-input" type="text" maxlength="5" placeholder="Enter OTP"
  682. aria-label="OTP">
  683. <div style="height:12px"></div>
  684. <button id="submitOtpBtn" class="btn" type="button">Submit</button>
  685. </div>
  686. </div>
  687. </div>
  688. </div>
  689. </div>
  690. </main>
  691. </div>
  692. <div id="pageLoader"
  693. style="display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(255,255,255,0.8); z-index: 9999; text-align: center;">
  694. <div style="position: relative; top: 50%; transform: translateY(-50%);">
  695. <div class="spinner-border text-primary" style="width: 3rem; height: 3rem;"></div>
  696. <p style="margin-top: 1rem; font-size: 1.2rem; color: #007bff;">Please wait...</p>
  697. </div>
  698. </div>
  699. <script th:replace="~{fragments/user-script :: script}"></script>
  700. <script>
  701. $(document).ready(function () {
  702. $('#otpInput').on('input', function () {
  703. // Allow only digits
  704. this.value = this.value.replace(/\D/g, '');
  705. // Max 10 digits
  706. if (this.value.length > 5) {
  707. this.value = this.value.slice(0, 5);
  708. }
  709. });
  710. });
  711. /* ========================
  712. MOBILE NAV (improved)
  713. ======================== */
  714. (function () {
  715. const hambtn = document.getElementById('hambtn');
  716. const mobileNav = document.getElementById('mobileNav');
  717. const mobileOverlay = document.getElementById('mobileOverlay');
  718. const desktopBook = document.getElementById('desktopBookBtn');
  719. const mobileBook = document.getElementById('mobileBook');
  720. if (!hambtn || !mobileNav || !mobileOverlay) {
  721. // nothing to do if elements missing
  722. } else {
  723. function openMenu() {
  724. hambtn.setAttribute('aria-expanded', 'true');
  725. mobileNav.classList.add('open'); mobileNav.setAttribute('aria-hidden', 'false');
  726. mobileOverlay.classList.add('show'); mobileOverlay.setAttribute('aria-hidden', 'false');
  727. document.body.style.overflow = 'hidden';
  728. const first = mobileNav.querySelector('a'); if (first) first.focus();
  729. }
  730. function closeMenu() {
  731. hambtn.setAttribute('aria-expanded', 'false');
  732. mobileNav.classList.remove('open'); mobileNav.setAttribute('aria-hidden', 'true');
  733. mobileOverlay.classList.remove('show'); mobileOverlay.setAttribute('aria-hidden', 'true');
  734. document.body.style.overflow = '';
  735. hambtn.focus();
  736. }
  737. hambtn.addEventListener('click', () => {
  738. const isOpen = hambtn.getAttribute('aria-expanded') === 'true';
  739. if (isOpen) closeMenu(); else openMenu();
  740. });
  741. mobileOverlay.addEventListener('click', closeMenu);
  742. document.addEventListener('keydown', (e) => {
  743. if (e.key === 'Escape' && mobileNav.classList.contains('open')) closeMenu();
  744. });
  745. mobileNav.querySelectorAll('a').forEach(a => {
  746. a.addEventListener('click', () => {
  747. // close after clicking any mobile link
  748. closeMenu();
  749. });
  750. });
  751. // ensure menu closes when resizing to desktop
  752. window.addEventListener('resize', () => {
  753. if (window.innerWidth > 900 && mobileNav.classList.contains('open')) closeMenu();
  754. });
  755. }
  756. // Optional: wire desktop Book a Visit to the same booking action as mobile
  757. if (desktopBook && mobileBook) {
  758. desktopBook.addEventListener('click', (e) => {
  759. e.preventDefault();
  760. // mimic mobile behavior for demo: show alert (replace with real action)
  761. alert('Book a Visit clicked — open booking flow here.');
  762. });
  763. mobileBook.addEventListener('click', (e) => {
  764. // mobile link closes menu via handler above and then can navigate or open booking flow
  765. // if you want to override, uncomment below
  766. // e.preventDefault(); alert('Mobile Book clicked (replace with action).');
  767. });
  768. }
  769. })();
  770. /* ========================
  771. OTP PANEL (demo logic) - start timer only on explicit Resend click
  772. ======================== */
  773. (function () {
  774. const contactInput = document.getElementById('contactInput');
  775. const sendBtn = document.getElementById('sendBtn');
  776. const sentBlock = document.getElementById('sent-block');
  777. const inputBlock = document.getElementById('input-block');
  778. const contactInputDisabled = document.getElementById('contactInputDisabled');
  779. const sendBtnDisabled = document.getElementById('sendBtnDisabled');
  780. const changeLink = document.getElementById('changeLink');
  781. const resendLink = document.getElementById('resendLink');
  782. const otpArea = document.getElementById('otpArea');
  783. const otpInput = document.getElementById('otpInput');
  784. const submitOtpBtn = document.getElementById('submitOtpBtn');
  785. let resendTimer = null;
  786. let secondsLeft = 0;
  787. const RESEND_DELAY = 30; // seconds
  788. function isEmail(text) { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(text); }
  789. function isPhone(text) {
  790. const digits = text.replace(/\D/g, '');
  791. return digits.length === 10; // EXACT 10 digits only
  792. }
  793. function isValidContact(text) { if (!text) return false; const trimmed = text.trim(); return isEmail(trimmed) || isPhone(trimmed); }
  794. function setResendDisabled(state) {
  795. if (!resendLink) return;
  796. if (state) {
  797. resendLink.classList.add('disabled');
  798. resendLink.setAttribute('aria-disabled', 'true');
  799. resendLink.style.pointerEvents = 'none';
  800. } else {
  801. resendLink.classList.remove('disabled');
  802. resendLink.removeAttribute('aria-disabled');
  803. resendLink.style.pointerEvents = '';
  804. resendLink.textContent = 'Resend OTP';
  805. }
  806. }
  807. function startResendCountdown(sec = RESEND_DELAY) {
  808. if (!resendLink) return;
  809. if (resendTimer) { clearInterval(resendTimer); resendTimer = null; }
  810. secondsLeft = sec;
  811. // lock the resend link and show countdown
  812. setResendDisabled(true);
  813. resendLink.textContent = `Resend OTP (${secondsLeft}s)`;
  814. resendTimer = setInterval(() => {
  815. secondsLeft--;
  816. if (secondsLeft <= 0) {
  817. clearInterval(resendTimer);
  818. resendTimer = null;
  819. setResendDisabled(false);
  820. } else {
  821. resendLink.textContent = `Resend OTP (${secondsLeft}s)`;
  822. }
  823. }, 1000);
  824. }
  825. function demoSendOtp(contact) {
  826. $.ajax({
  827. type: "GET",
  828. url: '/victoriapublichall/otpapi/send-otp',
  829. data: { 'userInput': contact },
  830. success: function (response) {
  831. if (response.status === "success") {
  832. if (otpArea) { otpArea.style.display = 'block'; otpArea.setAttribute('aria-hidden', 'false'); }
  833. if (otpInput) { otpInput.value = ''; setTimeout(() => otpInput.focus(), 120); } startResendCountdown();
  834. } else if (response === "error") {
  835. Swal.fire({
  836. icon: 'warning',
  837. title: 'Error',
  838. text: 'Invalid Mobile Number or Email ID.',
  839. });
  840. return;
  841. }
  842. else {
  843. Swal.fire({
  844. icon: 'warning',
  845. title: 'Error',
  846. text: 'Error in OTP',
  847. });
  848. return;
  849. }
  850. },
  851. error: function () {
  852. alert("Error While Sending OTP.");
  853. }
  854. });
  855. }
  856. function showSentState(contact) {
  857. inputBlock.style.display = 'none'; inputBlock.setAttribute('aria-hidden', 'true');
  858. sentBlock.style.display = 'block'; sentBlock.setAttribute('aria-hidden', 'false');
  859. if (contactInputDisabled) contactInputDisabled.value = contact;
  860. if (sendBtnDisabled) sendBtnDisabled.disabled = true;
  861. // ensure resend link is enabled for the user to click (timer will only start on explicit click)
  862. setResendDisabled(false);
  863. }
  864. // Send OTP (initial send)
  865. if (sendBtn) {
  866. sendBtn.addEventListener('click', () => {
  867. const raw = (contactInput && contactInput.value) ? contactInput.value.trim() : '';
  868. // If only digits -> treat as phone
  869. if (/^\d+$/.test(raw) && raw.length !== 10) {
  870. // alert('Mobile number must be exactly 10 digits.');
  871. Swal.fire({
  872. icon: 'warning',
  873. title: 'Validation Error',
  874. text: 'Mobile number must be exactly 10 digits.',
  875. });
  876. contactInput.focus();
  877. return;
  878. }
  879. if (!isValidContact(raw)) {
  880. // alert('Enter a valid 10-digit phone number or email.');
  881. Swal.fire({
  882. icon: 'warning',
  883. title: 'Validation Error',
  884. text: 'Enter a valid 10-digit phone number or email.',
  885. });
  886. contactInput.focus();
  887. return;
  888. }
  889. showSentState(raw);
  890. // demo send OTP - do NOT start countdown here per requirement
  891. demoSendOtp(raw);
  892. document.getElementById('otpArea').scrollIntoView({
  893. behavior: 'smooth',
  894. block: 'start'
  895. });
  896. });
  897. }
  898. // Resend OTP - only starts countdown when user explicitly clicks Resend
  899. if (resendLink) {
  900. resendLink.addEventListener('click', (e) => {
  901. e.preventDefault();
  902. if (resendLink.classList.contains('disabled')) return; // safety
  903. const to = (contactInputDisabled && contactInputDisabled.value) ? contactInputDisabled.value : (contactInput && contactInput.value ? contactInput.value : '');
  904. if (!to) return;
  905. if (/^\d+$/.test(to) && to.length !== 10) {
  906. // alert('Mobile number must be exactly 10 digits.');
  907. Swal.fire({
  908. icon: 'warning',
  909. title: 'Validation Error',
  910. text: 'Mobile number must be exactly 10 digits.',
  911. });
  912. return;
  913. }
  914. if (!isValidContact(to)) {
  915. // alert('Enter a valid 10-digit phone number or email.');
  916. Swal.fire({
  917. icon: 'warning',
  918. title: 'Validation Error',
  919. text: 'Enter a valid 10-digit phone number or email.',
  920. });
  921. return;
  922. }
  923. // send new OTP and start the countdown now
  924. demoSendOtp(to);
  925. startResendCountdown();
  926. });
  927. }
  928. // Change link: go back to input state and reset
  929. if (changeLink) {
  930. changeLink.addEventListener('click', (e) => {
  931. e.preventDefault();
  932. if (resendTimer) { clearInterval(resendTimer); resendTimer = null; }
  933. setResendDisabled(false);
  934. sentBlock.style.display = 'none'; sentBlock.setAttribute('aria-hidden', 'true');
  935. inputBlock.style.display = 'block'; inputBlock.setAttribute('aria-hidden', 'false');
  936. if (contactInput) contactInput.focus();
  937. if (otpArea) { otpArea.style.display = 'none'; otpArea.setAttribute('aria-hidden', 'true'); }
  938. if (otpInput) otpInput.value = '';
  939. if (contactInput) contactInput.value = '';
  940. });
  941. }
  942. // Submit OTP
  943. if (submitOtpBtn) {
  944. submitOtpBtn.addEventListener('click', () => {
  945. const otp = otpInput ? otpInput.value.trim() : '';
  946. const userInput = (contactInputDisabled && contactInputDisabled.value) ? contactInputDisabled.value : (contactInput && contactInput.value ? contactInput.value : '');
  947. if (!otp) {
  948. Swal.fire({
  949. icon: 'warning',
  950. title: 'Missing OTP',
  951. text: 'Please Enter OTP',
  952. });
  953. otpInput.focus(); return;
  954. }
  955. if (!userInput) {
  956. Swal.fire({
  957. icon: 'warning',
  958. title: 'Missing Phone Number Or Email',
  959. text: 'Please Enter Phone Number Or Email',
  960. });
  961. otpInput.focus(); return;
  962. }
  963. /* if(otp === currentOtp){
  964. // demo success behavior: you can replace this with a redirect or server call
  965. setTimeout(()=> {
  966. alert('OTP verified (demo). You can now proceed to bookings.');
  967. // optional: automatically switch back to input or navigate
  968. // here we simulate closing the OTP flow (reset)
  969. changeLink.click();
  970. }, 700);
  971. } else {
  972. alert('Invalid OTP. Please try again.');
  973. otpInput.focus();
  974. } */
  975. $.ajax({
  976. type: "GET",
  977. url: '/victoriapublichall/otpapi/verifyOTP',
  978. data: { 'otp': otp, 'userInput': userInput },
  979. success: function (response) {
  980. console.log("API Response:", response);
  981. // Success case
  982. if (response.verifiedotp === "success") {
  983. Swal.fire({
  984. icon: 'success',
  985. title: response.message,
  986. //text: 'Click OK to continue',
  987. showConfirmButton: false,
  988. timer: 2000,
  989. allowOutsideClick: false,
  990. allowEscapeKey: false
  991. }).then(() => {
  992. console.log("Booking Details: ", response.regDetails);
  993. let hasBooking = false;
  994. let hasuser = response.userInput;
  995. // regDetails is an array → check if contains items
  996. if (response.regDetails && typeof response.regDetails === "object" && !Array.isArray(response.regDetails)) {
  997. hasBooking = true;
  998. console.log("step1");
  999. }
  1000. // regDetails is a string (No booking found) → treat as NO booking
  1001. else if (typeof response.regDetails === "string") {
  1002. hasBooking = false;
  1003. console.log("step2");
  1004. }
  1005. sessionStorage.setItem("hasBooking", hasBooking);
  1006. sessionStorage.setItem("hasuser", hasuser);
  1007. sessionStorage.setItem("bookingdetails", JSON.stringify(response.regDetails));
  1008. //sessionStorage.setItem("bookingdetails", response.regDetails);
  1009. // Redirect with the correctly determined flag
  1010. //window.location.href = "/victoriapublichall/registerbook?active=" + hasBooking+"user="+hasuser;
  1011. window.location.href = "/victoriapublichall/registerbook";
  1012. $('#pageLoader').show();
  1013. });
  1014. }
  1015. // Error case returned as 200 OK (badRequest still JSON)
  1016. else if (response.verifiedotp === "error") {
  1017. Swal.fire({
  1018. icon: 'error',
  1019. title: 'Invalid OTP',
  1020. text: response.message
  1021. });
  1022. }
  1023. },
  1024. error: function (xhr) {
  1025. console.log("Error Response:", xhr);
  1026. let msg = "Something went wrong";
  1027. // Extract JSON error response
  1028. try {
  1029. let err = JSON.parse(xhr.responseText);
  1030. msg = err.message;
  1031. } catch (e) {
  1032. msg = "Server error. Try again.";
  1033. }
  1034. Swal.fire({
  1035. icon: 'error',
  1036. title: 'Error',
  1037. text: msg
  1038. });
  1039. }
  1040. });
  1041. });
  1042. }
  1043. // keyboard shortcuts
  1044. if (contactInput) contactInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') sendBtn.click(); });
  1045. if (otpInput) otpInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') submitOtpBtn.click(); });
  1046. // input smoothing: if user types digits only, limit to 10 digits (for phone)
  1047. if (contactInput) {
  1048. contactInput.addEventListener('input', () => {
  1049. if (/^\d*$/.test(contactInput.value)) {
  1050. contactInput.value = contactInput.value.replace(/\D/g, '').slice(0, 10);
  1051. }
  1052. });
  1053. }
  1054. })();
  1055. </script>
  1056. </body>
  1057. </html>