diff --git a/public/spaces/fuajeeTalTech.glb b/public/spaces/fuajeeTalTech.glb new file mode 100644 index 0000000..04d3eff Binary files /dev/null and b/public/spaces/fuajeeTalTech.glb differ diff --git a/src/app/[locale]/messiala/page.tsx b/src/app/[locale]/messiala/page.tsx index 782ac79..3000e55 100644 --- a/src/app/[locale]/messiala/page.tsx +++ b/src/app/[locale]/messiala/page.tsx @@ -2,6 +2,7 @@ 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"; @@ -10,6 +11,7 @@ import { useTranslations } from "next-intl"; // 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() { @@ -17,6 +19,10 @@ export default function Expo() { const [hoveredRoom, setHoveredRoom] = useState(null); const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 }); const [showDividers, setShowDividers] = useState(true); + const [currentView, setCurrentView] = useState<"tudengimaja" | "fuajee">( + "tudengimaja", + ); + const currentViewRef = useRef<"tudengimaja" | "fuajee">("tudengimaja"); const t = useTranslations(); // Define room names with translations @@ -29,6 +35,15 @@ export default function Expo() { fighting: t("expo.areas.fighting"), lvlup: "LVLup!", redbull: "Red Bull", + // fuajee rooms + ityk: t("expo.areas.ityk"), + estoniagamedev: t("expo.areas.estoniagamedev"), + info: t("expo.areas.info"), + tartuyk: t("expo.areas.tartuyk"), + tly: t("expo.areas.tly"), + gameup: "GameUP!", + ittk: t("expo.areas.ittk"), + photobooth: t("expo.areas.photobooth"), }), [t], ); @@ -39,6 +54,10 @@ export default function Expo() { // 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(); @@ -62,7 +81,7 @@ export default function Expo() { // Isometric camera setup with responsive sizing const aspect = width / height; const baseFrustumSize = 14; - const frustumSize = width < 600 ? baseFrustumSize * 0.8 : baseFrustumSize; // Smaller frustum for mobile + const frustumSize = baseFrustumSize; // Keep consistent frustum size const camera = new THREE.OrthographicCamera( (frustumSize * aspect) / -2, (frustumSize * aspect) / 2, @@ -72,9 +91,21 @@ export default function Expo() { 1000, ); - // Position camera for isometric view - camera.position.set(10, 10, 14); - camera.lookAt(-1.4, 0, 0); + // 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 tudengimaja) + camera.position.copy(cameraPositions.tudengimaja.position); + camera.lookAt(cameraPositions.tudengimaja.lookAt); // Renderer const renderer = new THREE.WebGLRenderer({ antialias: true }); @@ -118,6 +149,7 @@ export default function Expo() { name: string; originalColor: number; originalScale: THREE.Vector3; + view: "tudengimaja" | "fuajee"; }> = []; const dividers: THREE.Mesh[] = []; @@ -229,6 +261,7 @@ export default function Expo() { name: roomDef.name, originalColor: roomDef.color, originalScale: room.scale.clone(), + view: "tudengimaja", }); }); @@ -283,14 +316,168 @@ export default function Expo() { ground2.receiveShadow = true; scene.add(ground2); + // Store tudengimaja objects (rooms, ground, dividers) + tudengimajaObjects = [...rooms, ground, ground2, ...dividers]; + + // 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 = false; // Initially hidden + + // 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(); + }, + (progress) => { + console.log( + "Loading progress:", + (progress.loaded / progress.total) * 100 + "%", + ); + }, + (error) => { + console.error("Error loading GLTF:", error); + }, + ); + + // Function to create example rooms for fuajee + const createfuajeeRooms = () => { + const fuajeeRoomColors = [ + 0x7b1642, // ITÜK - Cherry Red + 0x365591, // Light Blue - Tartu Ülikool + 0xa82838, // Red - Tallinna Ülikool + 0x183bbf, // Dark Blue - Eesti Gamedev + 0xd12e7d, // Purple - Taltech + 0x228b22, // Green - GameUP + 0xff6347, // Orange - Info + 0x20b2aa, // Light Sea Green - Photobooth + ]; + + const fuajeeRoomDefinitions = [ + { + width: 5, + height: 0.5, + depth: 3.5, + x: -6, + z: 2.8, + color: fuajeeRoomColors[0], + name: roomNames.ityk, + }, + { + width: 5, + height: 0.5, + depth: 2, + x: 2.2, + z: -1.5, + color: fuajeeRoomColors[1], + name: roomNames.tartuyk, + }, + { + width: 6, + height: 0.5, + depth: 2, + x: -5.8, + z: -1.2, + color: fuajeeRoomColors[3], + name: roomNames.estoniagamedev, + }, + { + width: 2, + height: 0.5, + depth: 2, + x: -1.5, + z: -1.5, + color: fuajeeRoomColors[6], + name: roomNames.info, + }, + { + width: 2, + height: 0.5, + depth: 1.5, + x: 6, + z: -1.7, + color: fuajeeRoomColors[2], + name: roomNames.tly, + }, + { + width: 2, + height: 0.5, + depth: 1.5, + x: 11, + z: -1.7, + color: fuajeeRoomColors[4], + name: roomNames.ittk, + }, + { + width: 2, + height: 0.5, + depth: 1.5, + x: 13.5, + z: -1.7, + color: fuajeeRoomColors[7], + name: roomNames.photobooth, + }, + { + width: 2, + height: 0.5, + depth: 1.5, + x: 8.5, + z: -1.7, + color: fuajeeRoomColors[5], + name: roomNames.gameup, + }, + ]; + + fuajeeRoomDefinitions.forEach((roomDef) => { + const geometry = new THREE.BoxGeometry( + roomDef.width, + roomDef.height, + roomDef.depth, + ); + const material = new THREE.MeshLambertMaterial({ + color: roomDef.color, + }); + + const room = new THREE.Mesh(geometry, material); + room.position.set(roomDef.x, roomDef.height / 2 + 2, roomDef.z); + room.castShadow = true; + room.receiveShadow = true; + room.userData = { name: roomDef.name, originalColor: roomDef.color }; + room.visible = false; // Initially hidden + + scene.add(room); + fuajeeRooms.push(room); + roomData.push({ + mesh: room, + name: roomDef.name, + originalColor: roomDef.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 = - newWidth < 600 ? baseFrustumSize * 0.8 : baseFrustumSize; + const newFrustumSize = baseFrustumSize; camera.left = (newFrustumSize * newAspect) / -2; camera.right = (newFrustumSize * newAspect) / 2; @@ -314,11 +501,121 @@ export default function Expo() { // Update mouse position for tooltip setMousePosition({ x: event.clientX, y: event.clientY }); - // Update raycaster - raycaster.setFromCamera(mouse, camera); - const intersects = raycaster.intersectObjects(rooms); + // 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 all rooms to original state + // 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, @@ -326,38 +623,48 @@ export default function Expo() { 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); + // 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 { - setHoveredRoom(null); + 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); + } } }; - // Add mouse event listener - renderer.domElement.addEventListener("mousemove", onMouseMove); - // Animation loop const animate = () => { requestAnimationFrame(animate); // Gentle floating animation for rooms - 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; - }); + 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); }; @@ -374,8 +681,9 @@ export default function Expo() { }); }; - // Expose toggle function to parent scope + // Expose functions to parent scope mountElement.toggleDividers = toggleDividers; + mountElement.switchView = switchView; // Cleanup return () => { @@ -395,6 +703,16 @@ export default function Expo() { } }, [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 (
@@ -405,112 +723,248 @@ export default function Expo() {

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

-
-
-
- - {t("expo.areas.bar")} - -
-
-
- - EVAL - -
-
-
- - {t("expo.areas.boardGames")} - -
-
-
- - LVLup! - -
-
-
- - Red Bull - -
-
-
- - {t("expo.areas.simRacing")} - -
-
-
- - Sony - + + {currentView === "tudengimaja" && ( +
+
+
+ + {t("expo.areas.bar")} + +
+
+
+ + EVAL + +
+
+
+ + {t("expo.areas.boardGames")} + +
+
+
+ + LVLup! + +
+
+
+ + Red Bull + +
+
+
+ + {t("expo.areas.simRacing")} + +
+
+
+ + Sony + +
+
+
+ + {t("expo.areas.fighting")} + +
-
-
- - {t("expo.areas.fighting")} - + )} + + {currentView === "fuajee" && ( +
+
+
+ + {t("expo.areas.ityk")} + +
+
+
+ + {t("expo.areas.tartuyk")} + +
+
+
+ + {t("expo.areas.estoniagamedev")} + +
+
+
+ + {t("expo.areas.tly")} + +
+
+
+ + {t("expo.areas.ittk")} + +
+
+
+ + {t("expo.areas.info")} + +
+
+
+ + {t("expo.areas.gameup")} + +
+
+
+ + {t("expo.areas.photobooth")} + +
-
+ )} +
-
-
- )} - {showDividers ? t("expo.hide") : t("expo.show")} - + {/* Right Arrow - Only show when on tudengimaja to go to fuajee */} + {currentView === "tudengimaja" && ( + + )} + + {currentView === "tudengimaja" && ( + + )} +
- {/* Tooltip */} - {hoveredRoom && ( -
- {hoveredRoom} -
- )} + {/* Tooltip - only show for current view */} + {hoveredRoom && + ((currentView === "tudengimaja" && + [ + roomNames.boardGames, + roomNames.bar, + roomNames.eval, + roomNames.simRacing, + roomNames.fighting, + roomNames.lvlup, + roomNames.redbull, + ].includes(hoveredRoom)) || + (currentView === "fuajee" && + [ + roomNames.ityk, + roomNames.tartuyk, + roomNames.estoniagamedev, + roomNames.info, + roomNames.tly, + roomNames.ittk, + roomNames.photobooth, + roomNames.gameup, + ].includes(hoveredRoom))) && ( +
+ {hoveredRoom} +
+ )}
diff --git a/src/components/ui/skeleton.tsx b/src/components/ui/skeleton.tsx new file mode 100644 index 0000000..32ea0ef --- /dev/null +++ b/src/components/ui/skeleton.tsx @@ -0,0 +1,13 @@ +import { cn } from "@/lib/utils" + +function Skeleton({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} + +export { Skeleton } diff --git a/translations/en.json b/translations/en.json index 98e2422..6b96725 100644 --- a/translations/en.json +++ b/translations/en.json @@ -146,7 +146,8 @@ "registrationSetup": "Registration and setup in auditorium", "auditorium": "Auditorium", "studentHouse": "Student House (Tudengimaja)", - "auditoriumAndStudentHouse": "Auditorium and Student House" + "auditoriumAndStudentHouse": "Auditorium and Student House", + "entranceHall": "Entrance Hall" } }, "stream": { @@ -175,7 +176,15 @@ "bar": "Bar Area", "boardGames": "Board Games Area", "simRacing": "Red Bull Sim Racing", - "fighting": "Fighting Games Area" + "fighting": "Fighting Games Area", + "photobooth": "Photo booth", + "ityk": "TalTech IT Faculty Student Council", + "tartuyk": "Tartu University", + "estoniagamedev": "Estonia Gamedev", + "info": "Information booth", + "tly": "Tallinn University", + "ittk": "TalTech School of Information Technologies", + "gameup": "GameUP!" }, "hide": "Hide walls", "show": "Show walls" diff --git a/translations/et.json b/translations/et.json index e5c7d53..9269bb7 100644 --- a/translations/et.json +++ b/translations/et.json @@ -146,7 +146,8 @@ "registrationSetup": "Registreerimine ja setup aulas", "auditorium": "Aula", "studentHouse": "Tudengimaja", - "auditoriumAndStudentHouse": "Aula ja Tudengimaja" + "auditoriumAndStudentHouse": "Aula ja Tudengimaja", + "entranceHall": "Fuajee" } }, "stream": { @@ -175,7 +176,15 @@ "bar": "Baariala", "boardGames": "Lauamängude ala", "simRacing": "Red Bull Sim Racing", - "fighting": "Võitlusmängu ala" + "fighting": "Võitlusmängu ala", + "photobooth": "Fotoboks", + "ityk": "IT-teaduskonna üliõpilaskogu", + "tartuyk": "Tartu Ülikool", + "estoniagamedev": "Eesti Gamedev", + "info": "Infoboks", + "tly": "Tallinna Ülikool", + "ittk": "TalTech IT-Teaduskond", + "gameup": "GameUP!" }, "hide": "Peida seinad", "show": "Näita seinu"