mirror of
https://github.com/Lapikud/tipilan.git
synced 2026-09-22 15:22:59 +00:00
Add expo area, card zoom on mobile devices
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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€.",
|
||||||
|
|||||||
@@ -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€.",
|
||||||
|
|||||||
Reference in New Issue
Block a user