Add expo area, card zoom on mobile devices

This commit is contained in:
2026-09-09 01:08:56 +03:00
parent cc2db0b24f
commit 334a4ed9d5
4 changed files with 94 additions and 25 deletions

View File

@@ -1,13 +1,95 @@
"use client";
import React from "react";
import { vipnagorgialla } from "@/components/Vipnagorgialla"; import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Image from "next/image"; import Image from "next/image";
import Link from "next/link"; import Link from "next/link";
import { getTranslations, setRequestLocale } from "next-intl/server"; import { useTranslations } from "next-intl";
import { useState } from "react";
import { developers, universities } from "@/data/expo"; import { developers, universities } from "@/data/expo";
/** Thick semi-transparent cyan rule that separates the full-bleed sections. */ /** Thick semi-transparent cyan rule that separates the full-bleed sections. */
const DIVIDER = "border-t-4 border-[rgba(0,163,224,0.5)]"; const DIVIDER = "border-t-4 border-[rgba(0,163,224,0.5)]";
/** Horizontal padding used by the padded sections (64px at the design width). */ /** Horizontal padding used by the padded sections (64px at the design width). */
const GUTTER = "px-4 sm:px-8 lg:px-16"; const GUTTER = "px-6 md:px-16";
/** Map Modal Component */
function MapModal({ onClose, imageSrc, label, rotateMessage }: { onClose: () => void; imageSrc: string; label: string; rotateMessage: string }) {
return (
<div
className="fixed inset-0 bg-[#0E0F19]/90 z-[10000] flex items-center justify-center p-4 md:p-8"
onClick={onClose}
>
{/* Container for the map image - only shown in landscape */}
<div className="border-4 border-[#1F5673] bg-[rgba(0,163,224,0.1)] w-full max-w-[95vw] md:max-w-[90vw] [@media(orientation:portrait)]:hidden [@media(max-width:767px)]:hidden" onClick={(e) => e.stopPropagation()}>
<div className="relative w-full h-[80vh]">
<img
src={imageSrc}
alt={label}
className="w-full h-full object-contain p-2"
/>
</div>
</div>
{/* Portrait warning with border - only shown in portrait on small screens */}
<div className="border-4 border-[#1F5673] bg-[rgba(0,163,224,0.1)] p-8 max-w-[90vw] md:max-w-md text-[#EEE5E5] text-center text-base md:text-lg [@media(orientation:landscape)]:hidden [@media(min-width:768px)]:hidden pointer-events-none" onClick={(e) => e.stopPropagation()}>
{rotateMessage}
</div>
</div>
);
}
/** Client component for the maps section with modal functionality */
function MapsSection({ gutter }: { gutter: string }) {
const t = useTranslations();
const [selectedMap, setSelectedMap] = useState<{ image: string; label: string } | null>(null);
React.useEffect(() => {
if (selectedMap) {
document.body.style.overflow = "hidden";
} else {
document.body.style.overflow = "";
}
return () => {
document.body.style.overflow = "";
};
}, [selectedMap]);
const maps = [
{ image: "/images/messiala/fuajee-kaart.svg", label: t("expo.mapFoyer") },
{ image: "/images/messiala/tudengimaja-kaart.svg", label: t("expo.mapStudentHouse") },
];
return (
<>
<div className={`${gutter} pb-12 md:pb-16`}>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 md:gap-8">
{maps.map((map) => (
<button
key={map.image}
onClick={() => setSelectedMap(map)}
className="relative aspect-[3/2] md:aspect-[4/3] w-full cursor-zoom-in hover:brightness-110 transition"
aria-label={t("expo.viewMap")}
>
<img
src={map.image}
alt={map.label}
className="w-full h-full object-contain p-2 md:p-4 lg:p-8"
/>
</button>
))}
</div>
</div>
{selectedMap && (
<MapModal
onClose={() => setSelectedMap(null)}
imageSrc={selectedMap.image}
label={selectedMap.label}
rotateMessage={t("expo.rotateDevice")}
/>
)}
</>
);
}
/** Venue map card: the map SVG rendered transparent over the page background. */ /** Venue map card: the map SVG rendered transparent over the page background. */
function MapCard({ image, label }: { image: string; label: string }) { function MapCard({ image, label }: { image: string; label: string }) {
@@ -149,20 +231,14 @@ function GameCard({
); );
} }
export default async function Expo({ export default function Expo() {
params, const t = useTranslations();
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
setRequestLocale(locale);
const t = await getTranslations({ locale });
return ( return (
<div className="bg-[#0E0F19] min-h-screen"> <div className="bg-[#0E0F19] min-h-screen">
<div className="mx-auto w-full max-w-[1920px]"> <div className="mx-auto w-full">
{/* Page title */} {/* Page title */}
<div className={`${GUTTER} pt-12 md:pt-16 pb-8`}> <div className={`${GUTTER} pt-24 md:pt-32 pb-8`}>
<h1 <h1
className={`${vipnagorgialla.className} font-bold italic uppercase text-[#EEE5E5] leading-none text-[clamp(1.75rem,1.4rem+3vw,3.5rem)]`} className={`${vipnagorgialla.className} font-bold italic uppercase text-[#EEE5E5] leading-none text-[clamp(1.75rem,1.4rem+3vw,3.5rem)]`}
> >
@@ -171,18 +247,7 @@ export default async function Expo({
</div> </div>
{/* Venue maps */} {/* Venue maps */}
<div className={`${GUTTER} pb-12 md:pb-16`}> <MapsSection gutter={GUTTER} />
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 md:gap-8">
<MapCard
image="/images/messiala/fuajee-kaart.svg"
label={t("expo.mapFoyer")}
/>
<MapCard
image="/images/messiala/tudengimaja-kaart.svg"
label={t("expo.mapStudentHouse")}
/>
</div>
</div>
{/* Mini-tournaments feature (full-bleed) */} {/* Mini-tournaments feature (full-bleed) */}
<section <section

View File

@@ -41,7 +41,7 @@ const Header = ({ navItems }: HeaderProps) => {
const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false); const [isMobileMenuOpen, setIsMobileMenuOpen] = useState(false);
// Header navigation should include all options except homepage and hidden items // Header navigation should include all options except homepage and hidden items
const hiddenNavHrefs: NavItem["href"][] = ["/messiala"]; const hiddenNavHrefs: NavItem["href"][] = [];
const mainNavItems = navItems.filter( const mainNavItems = navItems.filter(
(item) => item.href !== "/" && !hiddenNavHrefs.includes(item.href), (item) => item.href !== "/" && !hiddenNavHrefs.includes(item.href),
); );

View File

@@ -870,6 +870,8 @@
"description": "The TipiLAN expo area hosts companies, additional activities and lectures.", "description": "The TipiLAN expo area hosts companies, additional activities and lectures.",
"mapFoyer": "Foyer map", "mapFoyer": "Foyer map",
"mapStudentHouse": "Student house map", "mapStudentHouse": "Student house map",
"viewMap": "View map",
"rotateDevice": "Please rotate your device to landscape for the best experience.",
"miniTournaments": { "miniTournaments": {
"title": "Mini-tournaments", "title": "Mini-tournaments",
"description": "TipiLAN hosts a variety of fun and competitive mini-tournaments. Regular visitors can take part too! The total prize pool across all tournaments is 1250€.", "description": "TipiLAN hosts a variety of fun and competitive mini-tournaments. Regular visitors can take part too! The total prize pool across all tournaments is 1250€.",

View File

@@ -871,6 +871,8 @@
"description": "TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad loengud.", "description": "TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad loengud.",
"mapFoyer": "Fuajee kaart", "mapFoyer": "Fuajee kaart",
"mapStudentHouse": "Tudengimaja kaart", "mapStudentHouse": "Tudengimaja kaart",
"viewMap": "Vaata kaarti",
"rotateDevice": "Palun pöörake seade horisontaalasendisse parema vaate jaoks.",
"miniTournaments": { "miniTournaments": {
"title": "Miniturniirid", "title": "Miniturniirid",
"description": "TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu tekitavaid miniturniire. Osaleda saavad ka niisama külastajad! Auhinnafond on kõigi turniiride peale 1250€.", "description": "TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu tekitavaid miniturniire. Osaleda saavad ka niisama külastajad! Auhinnafond on kõigi turniiride peale 1250€.",