diff --git a/next.config.ts b/next.config.ts
index e9ffa30..d6a1ca2 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -1,7 +1,24 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
- /* config options here */
+ async headers() {
+ return [
+ {
+ source: "/(.*)",
+ headers: [
+ {
+ key: "Content-Security-Policy",
+ value:
+ "frame-src 'self' https://tipilan.ee https://player.twitch.tv https://embed.twitch.tv; frame-ancestors 'self' https://tipilan.ee;",
+ },
+ {
+ key: "X-Frame-Options",
+ value: "SAMEORIGIN",
+ },
+ ],
+ },
+ ];
+ },
};
export default nextConfig;
diff --git a/package.json b/package.json
index 7c55e31..5f2e22a 100644
--- a/package.json
+++ b/package.json
@@ -27,7 +27,6 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"drizzle-orm": "^0.44.2",
- "gray-matter": "^4.0.3",
"lucide-react": "^0.522.0",
"material-symbols": "^0.31.8",
"next": "15.3.0",
@@ -36,7 +35,6 @@
"react-dom": "^19.1.1",
"react-icons": "^5.5.0",
"react-markdown": "^10.1.0",
- "remark-gfm": "^4.0.1",
"tailwind-merge": "^3.3.1",
"three": "^0.178.0",
"tw-animate-css": "^1.3.4"
diff --git a/src/app/kodukord/page.tsx b/src/app/kodukord/page.tsx
index 68cb8a1..fe6428d 100644
--- a/src/app/kodukord/page.tsx
+++ b/src/app/kodukord/page.tsx
@@ -1,55 +1,56 @@
// app/kodukord/page.tsx (App Router)
-import fs from "node:fs";
-import path from "node:path";
-import ReactMarkdown from "react-markdown";
-import remarkGfm from "remark-gfm";
-import {vipnagorgialla} from "@/components/Vipnagorgialla";
+import ReactMarkdown, { Components } from "react-markdown";
+import { vipnagorgialla } from "@/components/Vipnagorgialla";
import SectionDivider from "@/components/SectionDivider";
-export const runtime = "nodejs"; // ensure fs is available (not Edge)
-export const dynamic = "force-static"; // read at build time
+export default async function Page() {
+ const file = Bun.file("src/data/kodukord.md");
+ const content = await file.text();
-export default function Page() {
- const filePath = path.join(process.cwd(), "src/data", "kodukord.md");
- const content = fs.readFileSync(filePath, "utf8");
+ return (
+
+
+ {/* Page title (separate from markdown headings) */}
+
+ Kodukord
+
- return (
-
-
- {/* Page title (separate from markdown headings) */}
-
- Kodukord
-
-
-
-
(
-
- ),
- h2: ({node, ...props}) => (
-
- ),
- ol: ({node, ...props}) => (
-
- ),
- ul: ({node, ...props}) => (
-
- ),
- p: ({node, ...props}) => (
-
- ),
- }}
- >
- {content}
-
-
-
-
-
+
+
(
+
+ {props.children}
+
+ ),
+ h2: (props) => (
+
+ {props.children}
+
+ ),
+ ol: (props) => (
+
+ {props.children}
+
+ ),
+ ul: (props) => (
+
+ ),
+ p: (props) => {props.children}
,
+ } as Components
+ }
+ >
+ {content}
+
- );
+
+
+
+
+ );
}
diff --git a/src/app/messiala/page.tsx b/src/app/messiala/page.tsx
index c74a105..383344f 100644
--- a/src/app/messiala/page.tsx
+++ b/src/app/messiala/page.tsx
@@ -1,485 +1,499 @@
"use client";
-import {vipnagorgialla} from "@/components/Vipnagorgialla";
+import { vipnagorgialla } from "@/components/Vipnagorgialla";
import * as THREE from "three";
-import {useEffect, useRef, useState} from "react";
-import {EyeClosed, Eye} from "lucide-react";
-import SectionDivider from "@/components/SectionDivider";
+import { useEffect, useRef, useState } from "react";
+import { EyeClosed, Eye } from "lucide-react";
// Define interface for the ref with toggle function
interface MountRefCurrent extends HTMLDivElement {
- toggleDividers?: (show: boolean) => void;
+ toggleDividers?: (show: boolean) => 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);
-
- useEffect(() => {
- if (!mountRef.current) return;
-
- // Copy ref to variable to avoid stale closure in cleanup
- const mountElement = mountRef.current;
- let dividersRef: 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 = width < 600 ? baseFrustumSize * 0.8 : baseFrustumSize; // Smaller frustum for mobile
- const camera = new THREE.OrthographicCamera(
- (frustumSize * aspect) / -2,
- (frustumSize * aspect) / 2,
- frustumSize / 2,
- frustumSize / -2,
- 1,
- 1000,
- );
+ const mountRef = useRef(null);
+ const [hoveredRoom, setHoveredRoom] = useState(null);
+ const [mousePosition, setMousePosition] = useState({ x: 0, y: 0 });
+ const [showDividers, setShowDividers] = useState(true);
+
+ useEffect(() => {
+ if (!mountRef.current) return;
+
+ // Copy ref to variable to avoid stale closure in cleanup
+ const mountElement = mountRef.current;
+ let dividersRef: 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 = width < 600 ? baseFrustumSize * 0.8 : baseFrustumSize; // Smaller frustum for mobile
+ const camera = new THREE.OrthographicCamera(
+ (frustumSize * aspect) / -2,
+ (frustumSize * aspect) / 2,
+ frustumSize / 2,
+ frustumSize / -2,
+ 1,
+ 1000,
+ );
- // Position camera for isometric view
- camera.position.set(10, 10, 14);
- camera.lookAt(-1.4, 0, 0);
-
- // 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);
-
- // Room colors and names
- const roomColors = [
- 0xff6b35, // Orange - Mänguklubi
- 0x4ecdc4, // Turquoise - Baariala
- 0xffe66d, // Yellow - EVAL
- 0xe74c3c, // Red - Redbull
- 0x9b59b6, // Purple - Võitlusmängu ala
- 0x3498db, // Blue - Sony
- 0x2ecc71, // Green - Chillimisala
- ];
-
- const roomNames = [
- "Mänguklubi",
- "Baariala",
- "EVAL",
- "Redbull",
- "Võitlusmängu ala",
- "Sony",
- "Chillimisala",
- ];
-
- // Create individual rooms as rectangles with custom positions
- const rooms: THREE.Mesh[] = [];
- const roomData: Array<{
- mesh: THREE.Mesh;
- name: string;
- originalColor: number;
- originalScale: THREE.Vector3;
- }> = [];
- const dividers: THREE.Mesh[] = [];
-
- // Define rooms with custom positions, sizes and colors
- const roomDefinitions = [
- {
- width: 7,
- height: 0.7,
- depth: 3,
- x: 2.5,
- z: 4,
- color: roomColors[0],
- name: roomNames[0],
- }, // Mänguklubi
- // {
- // width: 2.5,
- // height: 0.7,
- // depth: 0.7,
- // x: 1,
- // z: 0,
- // color: roomColors[1],
- // name: roomNames[1],
- // }, // Baariala
- {
- width: 1.8,
- height: 0.7,
- depth: 1.5,
- x: 2.5,
- z: -3.5,
- color: roomColors[2],
- name: roomNames[2],
- }, // EVAL
- {
- width: 2.2,
- height: 0.7,
- depth: 4.5,
- x: 5,
- z: -2,
- color: roomColors[3],
- name: roomNames[3],
- }, // Redbull
- {
- width: 3,
- height: 0.7,
- depth: 1.3,
- x: 0,
- z: -3.5,
- color: roomColors[4],
- name: roomNames[4],
- }, // Võitlusmängu ala
- {
- width: 1.8,
- height: 0.7,
- depth: 1.5,
- x: -2.55,
- z: -3.5,
- color: roomColors[5],
- name: roomNames[5],
- }, // Sony
- {
- width: 4,
- height: 0.7,
- depth: 4,
- x: -5.5,
- z: -2.3,
- color: roomColors[6],
- name: roomNames[6],
- }, // Chillimisala
- ];
-
- roomDefinitions.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, roomDef.z);
- room.castShadow = true;
- room.receiveShadow = true;
- room.userData = {name: roomDef.name, originalColor: roomDef.color};
-
- scene.add(room);
- rooms.push(room);
- roomData.push({
- mesh: room,
- name: roomDef.name,
- originalColor: roomDef.color,
- originalScale: room.scale.clone(),
- });
- });
-
- // 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: 0x555555,
- 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(10, 2, 2, -2.5, 1.5); // Wall between main entrance
- createTogglableDivider(2, 2, 2, 5.5, 1.5); // Wall right next to Mänguklubi & Redbull
-
- // 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 groundMaterial2 = new THREE.MeshLambertMaterial({
- color: 0xcccccc,
- });
- const ground2 = new THREE.Mesh(groundGeometry2, groundMaterial2);
- ground2.rotation.x = -Math.PI / 2;
- ground2.position.x = -12.2;
- ground2.position.y = -5;
- ground2.receiveShadow = true;
- scene.add(ground2);
-
- // Resize handler
- const handleResize = () => {
- const {width: newWidth, height: newHeight} = getResponsiveDimensions();
-
- // Update camera
- const newAspect = newWidth / newHeight;
- const newFrustumSize =
- newWidth < 600 ? baseFrustumSize * 0.8 : 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});
-
- // Update raycaster
- raycaster.setFromCamera(mouse, camera);
- const intersects = raycaster.intersectObjects(rooms);
-
- // Reset all rooms to original state
- roomData.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);
- }
- };
-
- // 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;
- });
-
- 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 toggle function to parent scope
- mountElement.toggleDividers = toggleDividers;
-
- // Cleanup
- return () => {
- window.removeEventListener("resize", handleResize);
- renderer.domElement.removeEventListener("mousemove", onMouseMove);
- if (mountElement && renderer.domElement) {
- mountElement.removeChild(renderer.domElement);
- }
- renderer.dispose();
- };
- }, []);
-
- // Update dividers when showDividers state changes
- useEffect(() => {
- if (mountRef.current?.toggleDividers) {
- mountRef.current.toggleDividers(showDividers);
+ // Position camera for isometric view
+ camera.position.set(10, 10, 14);
+ camera.lookAt(-1.4, 0, 0);
+
+ // 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);
+
+ // Room colors and names
+ const roomColors = [
+ 0x343434, // Gray - Lauamängude ala
+ 0x4ecdc4, // Turquoise - Baariala
+ 0xffe66d, // Yellow - EVAL
+ 0xff6600, // Orange - Redbull Sim Racing
+ 0xff1493, // Deep Pink - Võitlusmängu ala
+ 0x3498db, // Blue - Sony
+ 0x2ecc71, // Green - Lava
+ 0x080682, // Dark Blue - LVLup!
+ 0xc02841, // Red - RedBull
+ ];
+
+ // Create individual rooms as rectangles with custom positions
+ const rooms: THREE.Mesh[] = [];
+ const roomData: Array<{
+ mesh: THREE.Mesh;
+ name: string;
+ originalColor: number;
+ originalScale: THREE.Vector3;
+ }> = [];
+ const dividers: THREE.Mesh[] = [];
+
+ // Define rooms with custom positions, sizes and colors
+ const roomDefinitions = [
+ {
+ width: 7,
+ height: 0.7,
+ depth: 3,
+ x: 2.5,
+ z: 4,
+ color: roomColors[0],
+ name: "Lauamängude ala",
+ },
+ {
+ width: 3.5,
+ height: 0.7,
+ depth: 1.2,
+ x: 0.7,
+ z: -0.3,
+ color: roomColors[1],
+ name: "Baariala",
+ },
+ {
+ width: 1.8,
+ height: 0.7,
+ depth: 1.5,
+ x: 1,
+ z: -3.5,
+ color: roomColors[2],
+ name: "EVAL",
+ },
+ {
+ width: 2,
+ height: 0.7,
+ depth: 4.5,
+ x: 5.2,
+ z: -2,
+ color: roomColors[3],
+ name: "Red Bull Sim Racing",
+ },
+ {
+ width: 3,
+ height: 0.7,
+ depth: 1.5,
+ x: -1.7,
+ z: -3.5,
+ color: roomColors[4],
+ name: "Võitlusmängu ala",
+ },
+ // {
+ // width: 1.8,
+ // height: 0.7,
+ // depth: 1.5,
+ // x: -4.3,
+ // z: -3.5,
+ // color: roomColors[5],
+ // name: "Sony",
+ // },
+ {
+ width: 3,
+ height: 0.7,
+ depth: 1.7,
+ x: -3.5,
+ z: -0.5,
+ color: roomColors[7],
+ name: "LVLup!",
+ },
+ //{
+ // width: 2,
+ // height: 0.7,
+ // depth: 4,
+ // x: -6.4,
+ // z: -2.3,
+ // color: roomColors[6],
+ // name: "Lava",
+ //},
+ {
+ width: 1.8,
+ height: 0.7,
+ depth: 1.5,
+ x: 3,
+ z: -3.5,
+ color: roomColors[8],
+ name: "Red Bull",
+ },
+ ];
+
+ roomDefinitions.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, roomDef.z);
+ room.castShadow = true;
+ room.receiveShadow = true;
+ room.userData = { name: roomDef.name, originalColor: roomDef.color };
+
+ scene.add(room);
+ rooms.push(room);
+ roomData.push({
+ mesh: room,
+ name: roomDef.name,
+ originalColor: roomDef.color,
+ originalScale: room.scale.clone(),
+ });
+ });
+
+ // 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: 0x555555,
+ 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(10, 2, 2, -2.5, 1.5); // Wall between main entrance
+ createTogglableDivider(2, 2, 2, 5.5, 1.5); // Wall right next to Lauamängud & Redbull Sim Racing
+
+ // 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 groundMaterial2 = new THREE.MeshLambertMaterial({
+ color: 0xcccccc,
+ });
+ const ground2 = new THREE.Mesh(groundGeometry2, groundMaterial2);
+ ground2.rotation.x = -Math.PI / 2;
+ ground2.position.x = -12.2;
+ ground2.position.y = -5;
+ ground2.receiveShadow = true;
+ scene.add(ground2);
+
+ // Resize handler
+ const handleResize = () => {
+ const { width: newWidth, height: newHeight } = getResponsiveDimensions();
+
+ // Update camera
+ const newAspect = newWidth / newHeight;
+ const newFrustumSize =
+ newWidth < 600 ? baseFrustumSize * 0.8 : 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 });
+
+ // Update raycaster
+ raycaster.setFromCamera(mouse, camera);
+ const intersects = raycaster.intersectObjects(rooms);
+
+ // Reset all rooms to original state
+ roomData.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);
}
- }, [showDividers]);
-
- return (
-
-
-
- Messiala
-
-
-
- Tudengimaja
-
-
-
-
-
-
+ } else {
+ setHoveredRoom(null);
+ }
+ };
+
+ // 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;
+ });
+
+ 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 toggle function to parent scope
+ mountElement.toggleDividers = toggleDividers;
+
+ // Cleanup
+ return () => {
+ window.removeEventListener("resize", handleResize);
+ renderer.domElement.removeEventListener("mousemove", onMouseMove);
+ if (mountElement && renderer.domElement) {
+ mountElement.removeChild(renderer.domElement);
+ }
+ renderer.dispose();
+ };
+ }, []);
+
+ // Update dividers when showDividers state changes
+ useEffect(() => {
+ if (mountRef.current?.toggleDividers) {
+ mountRef.current.toggleDividers(showDividers);
+ }
+ }, [showDividers]);
+
+ return (
+
+
+ Messiala
+
+
+
+ Tudengimaja
+
+
+
-
+
-
+
-
+
+
-
+
+
+
+ Red Bull Sim Racing
+
+
+
-
+
-
-
-
-
-
setShowDividers(!showDividers)}
- className={`absolute top-2 right-2 px-3 py-2 bg-[#1F5673] text-white hover:bg-[#2A7A9B] ${vipnagorgialla.className} uppercase italic text-sm font-semibold flex items-center transition-colors shadow-lg z-10`}
- >
- {showDividers ? (
-
- ) : (
-
- )}
-
- {showDividers ? "Peida" : "Näita"}
-
-
-
-
- {/* Tooltip */}
- {hoveredRoom && (
-
- {hoveredRoom}
-
- )}
-
-
-
-
+
- );
+
+
+
+
setShowDividers(!showDividers)}
+ className={`absolute top-2 right-2 px-3 py-2 bg-[#1F5673] text-white hover:bg-[#2A7A9B] ${vipnagorgialla.className} uppercase italic text-sm font-semibold flex items-center transition-colors shadow-lg z-10`}
+ >
+ {showDividers ? (
+
+ ) : (
+
+ )}
+
+ {showDividers ? "Peida" : "Näita"}
+
+
+
+
+ {/* Tooltip */}
+ {hoveredRoom && (
+
+ {hoveredRoom}
+
+ )}
+
+
+ );
}
diff --git a/src/app/reeglid/[slug]/page.tsx b/src/app/reeglid/[slug]/page.tsx
index 5e4f735..d58f47e 100644
--- a/src/app/reeglid/[slug]/page.tsx
+++ b/src/app/reeglid/[slug]/page.tsx
@@ -1,49 +1,103 @@
-import {vipnagorgialla} from "@/components/Vipnagorgialla";
-import path from "node:path";
-import fs from "node:fs/promises";
-import ReactMarkdown from "react-markdown";
+import { notFound } from "next/navigation";
+import ReactMarkdown, { Components } from "react-markdown";
+import { vipnagorgialla } from "@/components/Vipnagorgialla";
import SectionDivider from "@/components/SectionDivider";
-type Props = {
- params: Promise<{ slug: string }>;
-};
-
-export default async function RulePage({params}: Props) {
- const {slug} = await params;
-
- const filePath = path.join(process.cwd(), "src/data/rules", `${slug}.md`);
- let file: string;
-
- try {
- file = await fs.readFile(filePath, "utf8");
- } catch {
- file = `# ${slug.toUpperCase()} REEGLID\n\nSisu hetkel puudub.`;
- }
-
- const data = {title: undefined as string | undefined};
-
- return (
- <>
-
-
- {data.title || `${slug.toUpperCase()} REEGLID`}
-
-
-
- {file}
-
-
-
-
- >
- );
+// Map of valid slugs to their corresponding file paths and titles
+const rulesMap = {
+ lol: {
+ filePath: "src/data/rules/lol.md",
+ title: "LOL Reeglid",
+ },
+ cs2: {
+ filePath: "src/data/rules/cs2.md",
+ title: "CS2 Reeglid",
+ },
+} as const;
+
+type RuleSlug = keyof typeof rulesMap;
+
+interface PageProps {
+ params: Promise<{ slug: string }>;
+}
+
+async function getRuleContent(slug: string) {
+ if (!Object.keys(rulesMap).includes(slug)) {
+ return null;
+ }
+
+ const ruleConfig = rulesMap[slug as RuleSlug];
+
+ try {
+ const file = Bun.file(ruleConfig.filePath);
+ const content = await file.text();
+ return {
+ content,
+ title: ruleConfig.title,
+ };
+ } catch (error) {
+ console.error(`Error reading rule file for slug ${slug}:`, error);
+ return null;
+ }
+}
+
+export default async function RulePage({ params }: PageProps) {
+ const { slug } = await params;
+ const ruleData = await getRuleContent(slug);
+
+ if (!ruleData) {
+ notFound();
+ }
+
+ const headingStyle = `text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold uppercase italic text-[#2A2C3F] dark:text-[#EEE5E5]`;
+
+ return (
+
+
+
+ {ruleData.title}
+
+
+
+
(
+
+ {props.children}
+
+ ),
+ h2: (props) => (
+
+ {props.children}
+
+ ),
+ ol: (props) => (
+
+ {props.children}
+
+ ),
+ ul: (props) => (
+
+ ),
+ p: (props) => {props.children}
,
+ } as Components
+ }
+ >
+ {ruleData.content}
+
+
+
+
+
+
+ );
+}
+
+export async function generateStaticParams() {
+ return Object.keys(rulesMap).map((slug) => ({
+ slug,
+ }));
}
diff --git a/src/app/reeglid/page.tsx b/src/app/reeglid/page.tsx
index 4afaf1e..167893e 100644
--- a/src/app/reeglid/page.tsx
+++ b/src/app/reeglid/page.tsx
@@ -1,64 +1,52 @@
-import {vipnagorgialla} from "@/components/Vipnagorgialla";
+import { vipnagorgialla } from "@/components/Vipnagorgialla";
import Link from "next/link";
import SectionDivider from "@/components/SectionDivider";
export default function RulesMenu() {
- const headingStyle = `text-5xl sm:text-6xl ${vipnagorgialla.className} font-bold italic text-[#2A2C3F] dark:text-[#EEE5E5]`;
-
- const boxStyle = `-skew-x-2 md:-skew-x-5 text-white md:px-12 hover:scale-103 transition-all duration-150 w-full md:w-xl lg:w-[400px]`;
-
- const boxTextStyle = `text-3xl ${vipnagorgialla.className} font-bold uppercase text-[#EEE5E5] pb-2`;
+ const headingStyle = `text-4xl md:text-5xl lg:text-6xl ${vipnagorgialla.className} font-bold italic text-[#2A2C3F] dark:text-[#EEE5E5] uppercase`;
- // const SectionDivider = () =>
;
+ const boxStyle = `-skew-x-2 md:-skew-x-5 text-white md:px-12 hover:scale-103 transition-all duration-150 w-full md:w-xl lg:w-[400px]`;
- return (
-
-
-
- REEGLID
-
+ const boxTextStyle = `text-3xl ${vipnagorgialla.className} font-bold uppercase text-[#EEE5E5] pb-2`;
-
-
-
-
- Kodukord
-
-
-
+ // const SectionDivider = () =>
;
-
-
-
- CS2 reeglid
-
-
-
+ return (
+
+
+
+ REEGLID
+
+
+
+
+
Kodukord
+
+
-
-
-
- LoL reeglid
-
-
-
+
+
+
CS2 reeglid
+
+
- {/* Minitourn. link coming soon*/}
- {/*
*/}
-
-
- Miniturniiride reeglid
-
-
- {/**/}
-
-
+
+
+
LoL reeglid
-
-
-
+
+
+ {/* Minitourn. link coming soon*/}
+ {/*
*/}
+
+
Miniturniiride reeglid
+
+ {/**/}
- );
-}
\ No newline at end of file
+
+
+
+
+ );
+}
diff --git a/src/app/striim/page.tsx b/src/app/striim/page.tsx
new file mode 100644
index 0000000..1d1d7bf
--- /dev/null
+++ b/src/app/striim/page.tsx
@@ -0,0 +1,210 @@
+import { vipnagorgialla } from "@/components/Vipnagorgialla";
+import Link from "next/link";
+import Image from "next/image";
+
+export default function Home() {
+ return (
+
+
+ {/* Title */}
+
+
+
+
+
+
+ Ajakava
+
+
+ arrow_right_alt
+
+
+
+
+ event_note
+
+
+ TipiLAN on pungil põnevatest turniiridest, mini-võistlustest ja
+ paljust muust.
+
+
+
+
+ {/* Stream iframe from Twitch */}
+
+
+
+
+ {/* Grid of buttons */}
+
+
+
+
+ Turniirid
+
+
+ arrow_right_alt
+
+
+
+
+
+ trophy
+
+
+ TipiLANil toimuvad suurejoonelised CS2 ja LoL turniirid, mille
+ auhinnafond on 10 000€.
+
+
+
+
+
+
+ Messiala
+
+
+ arrow_right_alt
+
+
+
+
+ weekend
+
+
+ TipiLANi messialal paiknevad ettevõtted, lisategevused ja toimuvad
+ loengud.
+
+
+
+
+ {/* Date */}
+
+
+
+ Broneeri oma koht juba täna!
+
+
+ arrow_right_alt
+
+
+
+ 24.-26. okt.
+
+
+ {/* Sponsors */}
+
+
+
+ TipiLANi tõmbab käima...
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/src/app/turniirid/page.tsx b/src/app/turniirid/page.tsx
index dfaa889..084dce2 100644
--- a/src/app/turniirid/page.tsx
+++ b/src/app/turniirid/page.tsx
@@ -4,182 +4,178 @@ import Image from "next/image";
import SectionDivider from "@/components/SectionDivider";
export default function Tourney() {
- const headingStyle = `text-3xl md:text-5xl lg:text-5xl ${vipnagorgialla.className} font-bold uppercase text-[#2A2C3F] dark:text-[#EEE5E5] -skew-x-2 md:-skew-x-5`;
+ const headingStyle = `text-3xl md:text-5xl lg:text-5xl ${vipnagorgialla.className} font-bold uppercase text-[#2A2C3F] dark:text-[#EEE5E5] -skew-x-2 md:-skew-x-5`;
- // const SectionDivider = () =>
;
+ // const SectionDivider = () =>
;
- return (
-
-
- Turniirid
-
+ return (
+
+
+ Turniirid
+
- {/*
*/}
- {/* Kui tahate oma oskusi proovile panna, siis vaadake siia tagasi! Rohkem*/}
- {/* infot lähiajal.*/}
- {/*
*/}
-
-
- {/* CS2 turniir */}
-
-
-
- CS2 turniir
-
-
- Toimumisaeg veel selgumisel
-
-
- TipiLANil toimub Eesti ühe suurima auhinnafondiga CS2 turniire juba sel sügisel. Haara kaasa
- sõbrad ja
- saa osa adrenaliinirohkest kogemusest!
-
-
-
- Auhinnafond on suuruses 5250€, mis jaotatakse TOP3 meeskonna vahel ära. Iga tiimiliige saab
- vastavalt
- saavutatud kohale auhinnaks kas 600€, 300€ või 150€.
-
-
+ {/*
*/}
+ {/* Kui tahate oma oskusi proovile panna, siis vaadake siia tagasi! Rohkem*/}
+ {/* infot lähiajal.*/}
+ {/*
*/}
-
-
-
-
- {/* Outside div needs to remain so that overflow won't occur*/}
-
-
-
-
+
+ {/* CS2 turniir */}
+
+
+
CS2 turniir
+
+ Toimumisaeg veel selgumisel
+
+
+ TipiLANil toimub Eesti ühe suurima auhinnafondiga CS2 turniire
+ juba sel sügisel. Haara kaasa sõbrad ja saa osa adrenaliinirohkest
+ kogemusest!
+
+
+
+ Auhinnafond on suuruses 5250€, mis jaotatakse TOP3 meeskonna vahel
+ ära. Iga tiimiliige saab vastavalt saavutatud kohale auhinnaks kas
+ 600€, 300€ või 150€.
+
+
-
-
- {/* LoL turniir */}
-
-
-
- {/* Outside div needs to remain so that overflow won't occur*/}
-
-
-
-
-
- LoL turniir
-
-
- Toimumisaeg veel selgumisel
-
-
- TipiLANil toimub Eesti ühe suurima auhinnafondiga LoL turniire juba sel sügisel.
- Haara kaasa sõbrad ja saa osa adrenaliinirohkest kogemusest!
-
-
-
- Auhinnafond on suuruses 3500€, mis jaotatakse TOP3 meeskonna vahel ära. Iga tiimiliige saab
- vastavalt saavutatud kohale auhinnaks kas 400€, 200€ või 100€.
-
-
-
+
+
+
+
+ {/* Outside div needs to remain so that overflow won't occur*/}
+
+
+
+
-
-
-
+
-
+ {/* LoL turniir */}
+
+
+
+ {/* Outside div needs to remain so that overflow won't occur*/}
+
+
+
+
+
LoL turniir
+
+ Toimumisaeg veel selgumisel
+
+
+ TipiLANil toimub Eesti ühe suurima auhinnafondiga LoL turniire
+ juba sel sügisel. Haara kaasa sõbrad ja saa osa adrenaliinirohkest
+ kogemusest!
+
+
+
+ Auhinnafond on suuruses 3500€, mis jaotatakse TOP3 meeskonna vahel
+ ära. Iga tiimiliige saab vastavalt saavutatud kohale auhinnaks kas
+ 400€, 200€ või 100€.
+
+
+
+
+
- {/* Mini-turniirid */}
-
-
-
- Miniturniirid
-
-
- Toimumisaeg veel selgumisel
-
-
- TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu tekitavaid miniturniire.
- Miniturniirid toimuvad järgnevates mängudes: SimRacing, Tekken, FIFA, Minecraft Bedwars,
- Buckshot Roulette, LostGamer ja palju muud.
-
-
-
- Auhinnafond on kõigi turniiride peale 1250€ ja reeglina saab rahalise auhinna miniturniiri võitja.
-
-
-
-
-
-
- {/* Outside div needs to remain so that overflow won't occur*/}
-
-
-
-
+
-
+ {/* Mini-turniirid */}
+
+
+
Miniturniirid
+
+ Toimumisaeg veel selgumisel
+
+
+ TipiLANil toimub mitmeid erinevaid lõbusaid ja võistlushimu
+ tekitavaid miniturniire. Miniturniirid toimuvad järgnevates
+ mängudes: SimRacing, Tekken, FIFA, Minecraft Bedwars, Buckshot
+ Roulette, LostGamer ja palju muud.
+
+
+
+ Auhinnafond on kõigi turniiride peale 1250€ ja reeglina saab
+ rahalise auhinna miniturniiri võitja.
+
+
+
+
+
+
+ {/* Outside div needs to remain so that overflow won't occur*/}
+
+
+
- );
-}
\ No newline at end of file
+
+
+
+
+ );
+}
diff --git a/src/components/ui/button.tsx b/src/components/ui/button.tsx
index a2df8dc..6c5e578 100644
--- a/src/components/ui/button.tsx
+++ b/src/components/ui/button.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
@@ -32,8 +32,8 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
function Button({
className,
@@ -43,17 +43,25 @@ function Button({
...props
}: React.ComponentProps<"button"> &
VariantProps
& {
- asChild?: boolean
+ asChild?: boolean;
}) {
- const Comp = asChild ? Slot : "button"
+ if (asChild) {
+ return (
+ )}
+ />
+ );
+ }
return (
-
- )
+ );
}
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/src/data/rules/cs2.md b/src/data/rules/cs2.md
new file mode 100644
index 0000000..bae40f8
--- /dev/null
+++ b/src/data/rules/cs2.md
@@ -0,0 +1,5 @@
+## Tulekul
+
+CS2 turniiri reeglid on hetkel ettevalmistamisel ja avaldatakse peagi.
+
+Jälgige meie discordi!