Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions public/images/frame/Frame-maps.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
32,708 changes: 32,708 additions & 0 deletions public/images/maps/maps-chula-henri.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
30,580 changes: 30,580 additions & 0 deletions public/images/maps/maps-chula-pyt.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
13,937 changes: 13,937 additions & 0 deletions public/images/maps/maps-chula-samyan.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
834 changes: 834 additions & 0 deletions public/images/maps/maps-chula-siam.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
16,629 changes: 16,629 additions & 0 deletions public/images/maps/maps-main.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
88 changes: 88 additions & 0 deletions src/components/firstdate/MapDropdown.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
---
import { ChevronDown } from '@lucide/astro';

export interface Props {
options: { label: string; value: string; }[];
selectedOption: { label: string; value: string; };
class?: string;
}

const { options, selectedOption, class: additionalClasses = '' } = Astro.props;
---

<div class={`relative ${additionalClasses}`}>
<button
class="flex items-center justify-between w-full text-white font-semibold text-lg leading-none fill-white drop-shadow-lg drop-shadow-white/50"
id="map-dropdown-trigger"
aria-expanded="false"
aria-haspopup="true"
>
<span class="text-white drop-shadow-lg px-2">
{selectedOption.label}
</span>
<div class="w-10 h-10 flex items-center justify-center">
<ChevronDown size={18} color="white" />
</div>
</button>

<div
class="absolute top-full left-0 right-0 mt-1 bg-white/90 backdrop-blur-sm rounded-lg shadow-lg border border-gray-200 z-10 hidden"
id="map-dropdown-menu"
>
<ul class="py-1">
{options.map((option) => (
<li>
<button
class="w-full px-4 py-2 text-left text-gray-800 hover:bg-gray-100 transition-colors duration-200 text-lg"
data-value={option.value}
data-label={option.label}
>
{option.label}
</button>
</li>
))}
</ul>
</div>
</div>

<script>
document.addEventListener('DOMContentLoaded', function() {
const trigger = document.getElementById('map-dropdown-trigger');
const menu = document.getElementById('map-dropdown-menu');

if (trigger && menu) {
trigger.addEventListener('click', function() {
const isExpanded = trigger.getAttribute('aria-expanded') === 'true';
trigger.setAttribute('aria-expanded', (!isExpanded).toString());
menu.classList.toggle('hidden');
});

document.addEventListener('click', function(e) {
if (!trigger.contains(e.target) && !menu.contains(e.target)) {
trigger.setAttribute('aria-expanded', 'false');
menu.classList.add('hidden');
}
});

const options = menu.querySelectorAll('button[data-value]');
options.forEach(option => {
option.addEventListener('click', function() {
const value = this.getAttribute('data-value');
const label = this.getAttribute('data-label');
const span = trigger.querySelector('span');
if (span) {
span.textContent = label;
}
trigger.setAttribute('aria-expanded', 'false');
menu.classList.add('hidden');

// Dispatch custom event for map switching
const event = new CustomEvent('mapChanged', {
detail: { value, label }
});
document.dispatchEvent(event);
});
});
}
});
</script>
239 changes: 239 additions & 0 deletions src/pages/firstdate/maps/index.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
---
import MapDropdown from "@/components/firstdate/MapDropdown.astro";
import Layout from "@/layouts/firstdate/WithNavbar.astro";
import ButtonFd from "@/components/common/ButtonFd.astro";
import { Home } from '@lucide/astro';

const mapOptions = [
{ label: "แผนที่หลัก", value: "maps-main" },
{ label: "สามย่าน สวนหลวง", value: "maps-chula-samyan" },
{ label: "สยามลุมพินี", value: "maps-chula-siam" },
{ label: "จุฬาฯฝั่งใหญ่", value: "maps-chula-henri" },
{ label: "จุฬาฯฝั่งเล็ก", value: "maps-chula-pyt" }
];
const selectedMap = mapOptions[0];
---
<Layout>
<div class="flex flex-col w-full px-2.5 py-3 items-center justify-center">
<h1 class="text-4xl text-white font-zen-dots text-center fill-white drop-shadow-lg drop-shadow-white/50 my-8">Maps</h1>
<!-- Map Frame Container with integrated dropdown -->
<section class="relative flex-1 flex items-center justify-center">
<!-- Background Frame -->
<div class="relative w-full max-w-[90vw] sm:max-w-[356px] mx-auto" style="aspect-ratio: 356/497;">
<img
src="/images/frame/Frame-maps.svg"
alt="Map Frame"
class="w-full h-full object-contain"
/>

<!-- Dropdown positioned with responsive sizing -->
<div class="absolute left-3 top-3 z-10">
<MapDropdown
options={mapOptions}
selectedOption={selectedMap}
class="w-[200px] sm:w-[220px] border-b-2 sm:border-b-4 border-[#FFB6C1] "
/>
</div>

<!-- Map content with responsive sizing -->
<div class="absolute left-2 right-2 bottom-7 top-16 bg-white rounded-lg overflow-hidden">
<div
id="map-container"
class="w-full h-full relative cursor-grab active:cursor-grabbing overflow-hidden"
style="background-color: #efe7e3;"
data-current-map={selectedMap.value}
>
<!-- Map SVG -->
<div
id="map-content"
class="w-full h-full flex items-center justify-center transition-transform duration-200 ease-out"
style="transform: scale(1) translate(0px, 0px)"
>
<img
id="current-map"
src={`/images/maps/${selectedMap.value}.svg`}
alt={selectedMap.label}
class="max-w-none max-h-none object-contain"
style="width: auto; height: auto; min-width: 100%; min-height: 100%;"
/>
</div>

<!-- Zoom Controls -->
<div class="absolute bottom-2 right-2 flex flex-col gap-1 bg-white/90 backdrop-blur-sm rounded-lg p-1">
<button
id="zoom-in"
class="w-8 h-8 flex items-center justify-center bg-white hover:bg-gray-100 border border-gray-300 rounded text-gray-700 font-bold transition-colors"
>
+
</button>
<button
id="zoom-out"
class="w-8 h-8 flex items-center justify-center bg-white hover:bg-gray-100 border border-gray-300 rounded text-gray-700 font-bold transition-colors"
>
</button>
<button
id="reset-zoom"
class="w-8 h-8 flex items-center justify-center bg-white hover:bg-gray-100 border border-gray-300 rounded text-gray-700 text-xs transition-colors"
title="Reset zoom"
>
</button>
</div>
</div>
</div>
</div>
</section>
<!-- Map Description -->
<div class="my-4 space-y-4 text-center">
<h1 id="map-title" class="text-2xl font-medium mt-8-">{selectedMap.label}</h1>
<ButtonFd variant="fill" color="white" href="/firstdate/home/">
<Home slot="icon" size={20} />
กลับหน้าหลัก
</ButtonFd>
</div>

</div>
</Layout>

<script>
document.addEventListener('DOMContentLoaded', function() {
const mapContainer = document.getElementById('map-container');
const mapContent = document.getElementById('map-content');
const currentMap = document.getElementById('current-map');
const zoomInBtn = document.getElementById('zoom-in');
const zoomOutBtn = document.getElementById('zoom-out');
const resetZoomBtn = document.getElementById('reset-zoom');

if (!mapContainer || !mapContent || !currentMap) return;

let scale = 1;
let translateX = 0;
let translateY = 0;
let isDragging = false;
let startX = 0;
let startY = 0;
let startTranslateX = 0;
let startTranslateY = 0;

const minScale = 1; // Changed from 0.5 to 1 to prevent zoom out
const maxScale = 4;
const zoomStep = 0.3;

function updateTransform() {
mapContent.style.transform = `scale(${scale}) translate(${translateX}px, ${translateY}px)`;
// Update cursor - always allow grab since we can pan at any zoom level
mapContainer.style.cursor = 'grab';
}

function resetTransform() {
scale = 1;
translateX = 0;
translateY = 0;
updateTransform();
}

function constrainTranslation() {
const containerRect = mapContainer.getBoundingClientRect();
const contentRect = mapContent.getBoundingClientRect();

const maxTranslateX = Math.max(0, (contentRect.width - containerRect.width) / 2 / scale);
const maxTranslateY = Math.max(0, (contentRect.height - containerRect.height) / 2 / scale);

translateX = Math.max(-maxTranslateX, Math.min(maxTranslateX, translateX));
translateY = Math.max(-maxTranslateY, Math.min(maxTranslateY, translateY));
}

// Zoom functionality
zoomInBtn?.addEventListener('click', () => {
scale = Math.min(maxScale, scale + zoomStep);
constrainTranslation();
updateTransform();
});

zoomOutBtn?.addEventListener('click', () => {
scale = Math.max(minScale, scale - zoomStep);
constrainTranslation();
updateTransform();
});

resetZoomBtn?.addEventListener('click', resetTransform);

mapContainer.addEventListener('wheel', (e) => {
e.preventDefault();
const delta = e.deltaY > 0 ? -zoomStep : zoomStep;
scale = Math.max(minScale, Math.min(maxScale, scale + delta));
constrainTranslation();
updateTransform();
});

// Mouse drag functionality
mapContainer.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
startTranslateX = translateX;
startTranslateY = translateY;
mapContainer.style.cursor = 'grabbing';
});

document.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const deltaX = (e.clientX - startX) / scale;
const deltaY = (e.clientY - startY) / scale;
translateX = startTranslateX + deltaX;
translateY = startTranslateY + deltaY;
constrainTranslation();
updateTransform();
});

document.addEventListener('mouseup', () => {
isDragging = false;
mapContainer.style.cursor = 'grab';
});

let touchStartX = 0;
let touchStartY = 0;
let touchStartTranslateX = 0;
let touchStartTranslateY = 0;

mapContainer.addEventListener('touchstart', (e) => {
if (e.touches.length === 1) {
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
touchStartTranslateX = translateX;
touchStartTranslateY = translateY;
}
});

mapContainer.addEventListener('touchmove', (e) => {
e.preventDefault();
if (e.touches.length === 1) {
const touch = e.touches[0];
const deltaX = (touch.clientX - touchStartX) / scale;
const deltaY = (touch.clientY - touchStartY) / scale;
translateX = touchStartTranslateX + deltaX;
translateY = touchStartTranslateY + deltaY;
constrainTranslation();
updateTransform();
}
});

// Map switching functionality
document.addEventListener('mapChanged', (e) => {
const { value, label } = e.detail;
currentMap.src = `/images/maps/${value}.svg`;
currentMap.alt = label;
mapContainer.setAttribute('data-current-map', value);

// Update the map title
const mapTitle = document.getElementById('map-title');
if (mapTitle) {
mapTitle.textContent = label;
}

resetTransform();
});
});
</script>
Loading