"use client"; import { vipnagorgialla } from "@/components/Vipnagorgialla"; import * as THREE from "three"; import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js"; import { useEffect, useRef, useState, useMemo } from "react"; import { EyeClosed, Eye } from "lucide-react"; import SectionDivider from "@/components/SectionDivider"; import { useTranslations } from "next-intl"; import { roomNameKeys, staticRoomNames, roomMeta, RoomNameKey, } from "@/data/roomNames"; import gamedevData from "@/data/gamedev.json"; // Define interface for the ref with toggle function interface MountRefCurrent extends HTMLDivElement { toggleDividers?: (show: boolean) => void; switchView?: (view: "tudengimaja" | "fuajee") => void; } export default function Expo() { const mountRef = useRef(null); const [hoveredRoom, setHoveredRoom] = useState(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [showDividers, setShowDividers] = useState(true); const [currentView, setCurrentView] = useState<"tudengimaja" | "fuajee">( "fuajee", ); const currentViewRef = useRef<"tudengimaja" | "fuajee">("fuajee"); const t = useTranslations(); // Room names using translations and staticRoomNames const roomNames = useMemo(() => { const names: Record = {} as never; roomNameKeys.forEach((key) => { if (staticRoomNames[key]) { names[key] = staticRoomNames[key]!; } else { // fallback to translation key or just key names[key] = t(`expo.areas.${key}`, { default: key }); } }); return names; }, [t]); useEffect(() => { if (!mountRef.current) return; // Copy ref to variable to avoid stale closure in cleanup const mountElement = mountRef.current; let dividersRef: THREE.Mesh[] = []; const fuajeeMeshes: THREE.Mesh[] = []; let tudengimajaObjects: THREE.Object3D[] = []; let fuajeeMesh: THREE.Group | null = null; const fuajeeRooms: THREE.Mesh[] = []; // Scene setup const scene = new THREE.Scene(); scene.background = new THREE.Color(0x0e0f19); // Get responsive dimensions const getResponsiveDimensions = () => { const container = mountRef.current; if (!container) return { width: 800, height: 600 }; const containerWidth = container.offsetWidth; const maxWidth = Math.min(containerWidth, 800); const width = Math.max(maxWidth, 300); // Minimum width const height = (width * 600) / 800; // Maintain aspect ratio return { width, height }; }; const { width, height } = getResponsiveDimensions(); // Isometric camera setup with responsive sizing const aspect = width / height; const baseFrustumSize = 14; const frustumSize = baseFrustumSize; // Keep consistent frustum size const camera = new THREE.OrthographicCamera( (frustumSize * aspect) / -2, (frustumSize * aspect) / 2, frustumSize / 2, frustumSize / -2, 1, 1000, ); // Camera positions for different views const cameraPositions = { tudengimaja: { position: new THREE.Vector3(10, 10, 14), lookAt: new THREE.Vector3(-1.4, 0, 0), }, fuajee: { position: new THREE.Vector3(30, 20, 15), lookAt: new THREE.Vector3(0, 0, 0), }, }; // Position camera for isometric view (default to fuajee) camera.position.copy(cameraPositions.fuajee.position); camera.lookAt(cameraPositions.fuajee.lookAt); // Renderer const renderer = new THREE.WebGLRenderer({ antialias: true }); renderer.setSize(width, height); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; mountElement.appendChild(renderer.domElement); // Raycaster for mouse interactions const raycaster = new THREE.Raycaster(); const mouse = new THREE.Vector2(); // Lighting const ambientLight = new THREE.AmbientLight(0x404040, 1.2); scene.add(ambientLight); const directionalLight = new THREE.DirectionalLight(0xffffff, 1.5); directionalLight.position.set(10, 10, 5); directionalLight.castShadow = false; directionalLight.shadow.mapSize.width = 2048; directionalLight.shadow.mapSize.height = 2048; scene.add(directionalLight); // Create individual rooms as rectangles with custom positions using roomMeta const rooms: THREE.Mesh[] = []; const roomData: Array<{ mesh: THREE.Mesh; name: string; originalColor: number; originalScale: THREE.Vector3; view: "tudengimaja" | "fuajee"; }> = []; const dividers: THREE.Mesh[] = []; // Generate rooms for tudengimaja and fuajee using roomMeta roomNameKeys.forEach((key) => { const metas = roomMeta[key]; if (!metas) return; metas.forEach((meta) => { if (meta.view !== "tudengimaja") return; const geometry = new THREE.BoxGeometry( meta.size.width, meta.size.height, meta.size.depth, ); const material = new THREE.MeshLambertMaterial({ color: meta.color, }); const room = new THREE.Mesh(geometry, material); room.position.set(meta.position.x, meta.position.y, meta.position.z); room.castShadow = true; room.receiveShadow = true; room.userData = { name: roomNames[key], originalColor: meta.color }; scene.add(room); rooms.push(room); roomData.push({ mesh: room, name: roomNames[key], originalColor: meta.color, originalScale: room.scale.clone(), view: "tudengimaja", }); }); }); // Create toggleable room dividers const createTogglableDivider = ( width: number, height: number, depth: number, x: number, z: number, ) => { const wallGeometry = new THREE.BoxGeometry(width, height, depth); const wallMaterial = new THREE.MeshLambertMaterial({ color: 0x2e5570, // transparent: true, // opacity: 0, }); const wall = new THREE.Mesh(wallGeometry, wallMaterial); wall.position.set(x, height / 2, z); wall.visible = false; scene.add(wall); dividers.push(wall); }; // Add strategic dividers between major areas createTogglableDivider(2, 2, 1, -6.5, 1); // Wall behind photowall createTogglableDivider(4, 2, 2, -3.5, 1.5); // Wall between main entrance createTogglableDivider(2, 2, 1, -0.5, 1.5); // Wall behind bar createTogglableDivider(2, 2, 2, 1.5, 1.5); // Wall between main entrance // Store dividers reference for later access dividersRef = [...dividers]; // Ground plane const groundGeometry = new THREE.PlaneGeometry(14, 10.5); const groundMaterial = new THREE.MeshLambertMaterial({ color: 0xcccccc }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.position.x = -1.1; ground.position.y = -0.5; ground.receiveShadow = true; scene.add(ground); // Second ground plane const groundGeometry2 = new THREE.PlaneGeometry(2, 7); const ground2 = new THREE.Mesh(groundGeometry2, groundMaterial); ground2.rotation.x = -Math.PI / 2; ground2.position.x = -12.2; ground2.position.y = -5; ground2.receiveShadow = true; scene.add(ground2); // Store tudengimaja objects (rooms, ground, dividers) tudengimajaObjects = [...rooms, ground, ground2, ...dividers]; // Set initial visibility for fuajee default view tudengimajaObjects.forEach((obj) => (obj.visible = false)); // Load fuajee GLTF model const loader = new GLTFLoader(); loader.load( "/spaces/fuajeeTalTech.glb", (gltf) => { fuajeeMesh = gltf.scene; fuajeeMesh.position.set(-1.5, 1, 0); fuajeeMesh.scale.set(0.3, 0.3, 0.3); fuajeeMesh.visible = true; // Initially visible for fuajee default // Traverse the model to collect meshes fuajeeMesh.traverse((child) => { if (child instanceof THREE.Mesh) { child.castShadow = true; child.receiveShadow = true; fuajeeMeshes.push(child); } }); scene.add(fuajeeMesh); // Create example rooms for fuajee after the model loads createfuajeeRooms(); // Set initial visibility for fuajee view tudengimajaObjects.forEach((obj) => (obj.visible = false)); fuajeeMesh.visible = true; fuajeeRooms.forEach((room) => (room.visible = true)); }, (progress) => { console.log( "Loading progress:", (progress.loaded / progress.total) * 100 + "%", ); }, (error) => { console.error("Error loading GLTF:", error); }, ); // Function to create rooms for fuajee using roomMeta const createfuajeeRooms = () => { roomNameKeys.forEach((key) => { const metas = roomMeta[key]; if (!metas) return; metas.forEach((meta) => { if (meta.view !== "fuajee") return; const geometry = new THREE.BoxGeometry( meta.size.width, meta.size.height, meta.size.depth, ); const material = new THREE.MeshLambertMaterial({ color: meta.color, }); const room = new THREE.Mesh(geometry, material); room.position.set(meta.position.x, meta.position.y, meta.position.z); room.castShadow = true; room.receiveShadow = true; room.userData = { name: roomNames[key], originalColor: meta.color }; room.visible = true; // Initially visible for fuajee default scene.add(room); fuajeeRooms.push(room); roomData.push({ mesh: room, name: roomNames[key], originalColor: meta.color, originalScale: room.scale.clone(), view: "fuajee", }); }); }); }; // Resize handler const handleResize = () => { const { width: newWidth, height: newHeight } = getResponsiveDimensions(); // Update camera const newAspect = newWidth / newHeight; const newFrustumSize = baseFrustumSize; camera.left = (newFrustumSize * newAspect) / -2; camera.right = (newFrustumSize * newAspect) / 2; camera.top = newFrustumSize / 2; camera.bottom = newFrustumSize / -2; camera.updateProjectionMatrix(); // Update renderer renderer.setSize(newWidth, newHeight); }; // Add resize event listener window.addEventListener("resize", handleResize); // Mouse event handlers const onMouseMove = (event: MouseEvent) => { const rect = renderer.domElement.getBoundingClientRect(); mouse.x = ((event.clientX - rect.left) / rect.width) * 2 - 1; mouse.y = -((event.clientY - rect.top) / rect.height) * 2 + 1; // Update mouse position for tooltip setMousePosition({ x: event.clientX, y: event.clientY }); // Handle mouse interactions based on current view if (currentViewRef.current === "tudengimaja") { // Update raycaster raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(rooms); // Reset all tudengimaja rooms to original state roomData .filter((r) => r.view === "tudengimaja") .forEach(({ mesh, originalColor, originalScale }) => { (mesh.material as THREE.MeshLambertMaterial).color.setHex( originalColor, ); mesh.scale.copy(originalScale); }); if (intersects.length > 0) { const hoveredMesh = intersects[0].object as THREE.Mesh; const roomInfo = roomData.find((r) => r.mesh === hoveredMesh); if (roomInfo) { // Apply hover effects (hoveredMesh.material as THREE.MeshLambertMaterial).color.setHex( 0xffffff, ); hoveredMesh.scale.multiplyScalar(1.02); setHoveredRoom(roomInfo.name); } } else { setHoveredRoom(null); } } else if (currentViewRef.current === "fuajee") { // Update raycaster for fuajee rooms raycaster.setFromCamera(mouse, camera); const intersects = raycaster.intersectObjects(fuajeeRooms); // Reset all fuajee rooms to original state roomData .filter((r) => r.view === "fuajee") .forEach(({ mesh, originalColor, originalScale }) => { (mesh.material as THREE.MeshLambertMaterial).color.setHex( originalColor, ); mesh.scale.copy(originalScale); }); if (intersects.length > 0) { const hoveredMesh = intersects[0].object as THREE.Mesh; const roomInfo = roomData.find((r) => r.mesh === hoveredMesh); if (roomInfo) { // Apply hover effects with better visibility (hoveredMesh.material as THREE.MeshLambertMaterial).color.setHex( 0xffffff, ); hoveredMesh.scale.multiplyScalar(1.1); setHoveredRoom(roomInfo.name); } } else { setHoveredRoom(null); } } else { setHoveredRoom(null); } }; // Add mouse event listener renderer.domElement.addEventListener("mousemove", onMouseMove); // Function to switch camera views const switchView = (view: "tudengimaja" | "fuajee") => { const targetPosition = cameraPositions[view].position; const targetLookAt = cameraPositions[view].lookAt; // Animate camera transition const startPosition = camera.position.clone(); const startLookAt = new THREE.Vector3(); camera.getWorldDirection(startLookAt); startLookAt.multiplyScalar(-1).add(camera.position); let progress = 0; const animateCamera = () => { progress += 0.05; if (progress >= 1) { progress = 1; } // Smooth interpolation const easeProgress = 1 - Math.cos(progress * Math.PI * 0.5); camera.position.lerpVectors( startPosition, targetPosition, easeProgress, ); const currentLookAt = new THREE.Vector3().lerpVectors( startLookAt, targetLookAt, easeProgress, ); camera.lookAt(currentLookAt); if (progress < 1) { requestAnimationFrame(animateCamera); } }; animateCamera(); // Reset hover state when switching views setHoveredRoom(null); // Reset all room states to original roomData.forEach(({ mesh, originalColor, originalScale }) => { (mesh.material as THREE.MeshLambertMaterial).color.setHex( originalColor, ); mesh.scale.copy(originalScale); }); // Toggle visibility of objects based on view if (view === "fuajee") { tudengimajaObjects.forEach((obj) => (obj.visible = false)); if (fuajeeMesh) { fuajeeMesh.visible = true; } fuajeeRooms.forEach((room) => (room.visible = true)); } else { tudengimajaObjects.forEach((obj) => (obj.visible = true)); if (fuajeeMesh) { fuajeeMesh.visible = false; } fuajeeRooms.forEach((room) => (room.visible = false)); // Re-apply divider visibility state if (mountElement.toggleDividers) { mountElement.toggleDividers(showDividers); } } }; // Animation loop const animate = () => { requestAnimationFrame(animate); // Gentle floating animation for rooms if (currentViewRef.current === "tudengimaja") { rooms.forEach((room, index) => { const originalY = 0.25; // height / 2 for the room height of 0.5 const baseY = originalY + Math.sin(Date.now() * 0.001 + index) * 0.05; // Maintain current scale while updating Y position room.position.y = baseY; }); } else if (currentViewRef.current === "fuajee") { fuajeeRooms.forEach((room, index) => { const originalY = 2.25; // height / 2 for the room height of 0.5 + 2 offset const baseY = originalY + Math.sin(Date.now() * 0.001 + index) * 0.05; // Maintain current scale while updating Y position room.position.y = baseY; }); } renderer.render(scene, camera); }; animate(); // Function to toggle dividers const toggleDividers = (show: boolean) => { dividersRef.forEach((divider) => { divider.visible = show; (divider.material as THREE.MeshLambertMaterial).opacity = show ? 0.4 : 0; }); }; // Expose functions to parent scope mountElement.toggleDividers = toggleDividers; mountElement.switchView = switchView; // Cleanup return () => { window.removeEventListener("resize", handleResize); renderer.domElement.removeEventListener("mousemove", onMouseMove); if (mountElement && renderer.domElement) { mountElement.removeChild(renderer.domElement); } renderer.dispose(); }; }, [roomNames]); // Update dividers when showDividers state changes useEffect(() => { if (mountRef.current?.toggleDividers) { mountRef.current.toggleDividers(showDividers); } }, [showDividers]); // Handle view switching const handleViewSwitch = (view: "tudengimaja" | "fuajee") => { setCurrentView(view); currentViewRef.current = view; // Update ref immediately setHoveredRoom(null); // Clear any existing hover state if (mountRef.current?.switchView) { mountRef.current.switchView(view); } }; return (

{t("expo.title")}

{currentView === "tudengimaja" ? t("schedule.locations.studentHouse") : t("schedule.locations.entranceHall")}

{currentView === "tudengimaja" && (
{/* Bar */}
{t("expo.areas.bar")}
{/* EVAL */}
EVAL
{/* LVLup */}
LVLup!
{/* Red Bull */}
Red Bull
{/* Sim Racing */}
{t("expo.areas.simRacing")}
{/* Fighting */}
{t("expo.areas.fighting")}
{/* K-space */}
K-space.ee
{/* Photowall */}
{t("expo.areas.photowall")}
{/* Buckshot Roulette */}
Buckshot Roulette
{/* Chill Area */}
{t("expo.areas.chillArea")}
{/* Alzgamer */}
Alzgamer
{/* WC */}
WC
)} {currentView === "fuajee" && (
{t("expo.areas.estoniagamedev")}
{t("expo.areas.gameup")}
{t("expo.areas.info")}
{t("expo.areas.ittk")}
{t("expo.areas.studentformula")}
{t("expo.areas.tartuyk")}
{t("expo.areas.tly")}
)}
{/* Left Arrow - Only show when on fuajee to go back to tudengimaja */} {currentView === "fuajee" && ( )} {/* Right Arrow - Only show when on tudengimaja to go to fuajee */} {currentView === "tudengimaja" && ( )} {currentView === "tudengimaja" && ( )}
{/* Tooltip - only show for current view */} {hoveredRoom && ((currentView === "tudengimaja" && [ roomNames.boardGames, roomNames.bar, roomNames.eval, roomNames.simRacing, roomNames.fighting, roomNames.lvlup, roomNames.redbull, roomNames.kspace, roomNames.photowall, roomNames.buckshotroulette, roomNames.wc, roomNames.chillArea, roomNames.alzgamer, ].includes(hoveredRoom)) || (currentView === "fuajee" && [ roomNames.tartuyk, roomNames.estoniagamedev, roomNames.info, roomNames.tly, roomNames.ittk, roomNames.gameup, roomNames.studentformula, ].includes(hoveredRoom))) && (
{hoveredRoom}
)}
{/* MINITURNIIRID Section */}

MINITURNIIRID

TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu tekitavaid miniturniire. Osaleda saavad ka niisama külastajad! Auhinnafond on kõigi turniiride peale 1250€.

Miniturniirid logo
{/* PUHKA JA MÄNGI Section */}

PUHKA JA MÄNGI

{/* Card 1 - Chill-ala */}
Chill-ala

Chill-ala koos turniiride otseülekandega

{/* Card 2 - Mänguklubi */}
Mänguklubi

Mänguklubi lauamängud ja konsoolid

{/* Card 3 - Baariala */}
Baariala

Baariala jookide ja snäkkidega

{/* EESTI MÄNGUARENDAJAD Section */}

Eesti mänguarendajad

{gamedevData.games.map((game) => (
{game.name}

{game.name}

{game.developer}

))}
{/* ÜLIKOOLID Section */}

Ülikoolid

{/* First 12 games in 3x4 grid */}
{gamedevData.universities?.slice(0, 12).map((university) => (
{university.name}

{university.name}

{university.university}

))}
{/* Remaining games in new grid */} {gamedevData.universities && gamedevData.universities.length > 12 && (
{gamedevData.universities.slice(12).map((university) => (
{university.name}

{university.name}

{university.university}

))}
)}
); }