Compare commits

..

4 Commits

Author SHA1 Message Date
ff706e056d updated gitignore 2025-11-21 20:02:47 -07:00
2c472fec5c Add backgroundimg.avif to screenshot showcase carousel 2025-11-09 21:04:42 -07:00
3c2368d886 Fix API routes for Cloudflare Workers and enable preview URLs
- Remove unsupported next: { revalidate } option from API routes
- Add Cloudflare-specific cache configuration (cf.cacheTtl)
- Add User-Agent and Referer headers to bypass Plan API 403 errors
- Enable workers_dev and preview_urls in wrangler.jsonc to prevent disabling on deployment
2025-11-09 20:59:46 -07:00
d0bd636a43 Implement stylistic improvements: visual hierarchy, card depth, color accents, and layout balance
- Enhanced hero heading with size contrast, letter-spacing variation, and text shadows
- Added card depth utilities with inner shadows and varied shadow intensities
- Applied strategic color accents using primary theme variables
- Improved typography with better line-height and text shadows
- Standardized spacing with consistent gap and padding patterns
- Enhanced interactive states with smooth transforms and glow effects
- Added border styling variety with thickness and gradient borders
- Implemented background texture with noise pattern and backdrop enhancements
- Polished micro-interactions with smooth transitions
- Improved visual balance with adjusted proportions and dividers
- Updated fonts: Merriweather for headings, Chivo Mono for body text
- Removed navigation component and repositioned theme toggle
- Fixed ServerStatus component layout and removed double-card nesting
2025-11-09 20:47:57 -07:00
28 changed files with 16952 additions and 152 deletions

1
.gitignore vendored
View File

@ -43,3 +43,4 @@ next-env.d.ts
.dev.vars*
!.dev.vars.example
!.env.example
.specstory/**

File diff suppressed because it is too large Load Diff

367
package-lock.json generated
View File

@ -9,10 +9,12 @@
"version": "0.1.0",
"dependencies": {
"@opennextjs/cloudflare": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"html2canvas": "^1.4.1",
"lucide-react": "^0.552.0",
"minecraft-server-util": "^5.4.4",
"motion": "^12.23.24",
@ -9460,6 +9462,38 @@
"integrity": "sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==",
"license": "MIT"
},
"node_modules/@radix-ui/primitive": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
"integrity": "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==",
"license": "MIT"
},
"node_modules/@radix-ui/react-collection": {
"version": "1.1.7",
"resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.7.tgz",
"integrity": "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-compose-refs": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.2.tgz",
@ -9475,6 +9509,164 @@
}
}
},
"node_modules/@radix-ui/react-context": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.2.tgz",
"integrity": "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-direction": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.1.tgz",
"integrity": "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-dismissable-layer": {
"version": "1.1.11",
"resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.11.tgz",
"integrity": "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-escape-keydown": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-id": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.1.tgz",
"integrity": "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-navigation-menu": {
"version": "1.2.14",
"resolved": "https://registry.npmjs.org/@radix-ui/react-navigation-menu/-/react-navigation-menu-1.2.14.tgz",
"integrity": "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==",
"license": "MIT",
"dependencies": {
"@radix-ui/primitive": "1.1.3",
"@radix-ui/react-collection": "1.1.7",
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-context": "1.1.2",
"@radix-ui/react-direction": "1.1.1",
"@radix-ui/react-dismissable-layer": "1.1.11",
"@radix-ui/react-id": "1.1.1",
"@radix-ui/react-presence": "1.1.5",
"@radix-ui/react-primitive": "2.1.3",
"@radix-ui/react-use-callback-ref": "1.1.1",
"@radix-ui/react-use-controllable-state": "1.2.2",
"@radix-ui/react-use-layout-effect": "1.1.1",
"@radix-ui/react-use-previous": "1.1.1",
"@radix-ui/react-visually-hidden": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-presence": {
"version": "1.1.5",
"resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.5.tgz",
"integrity": "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-compose-refs": "1.1.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-primitive": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.1.3.tgz",
"integrity": "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-slot": "1.2.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-slot": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.2.3.tgz",
@ -9493,6 +9685,129 @@
}
}
},
"node_modules/@radix-ui/react-use-callback-ref": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.1.tgz",
"integrity": "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-controllable-state": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.2.2.tgz",
"integrity": "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-effect-event": "0.0.2",
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-effect-event": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-effect-event/-/react-use-effect-event-0.0.2.tgz",
"integrity": "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-layout-effect": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-escape-keydown": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.1.tgz",
"integrity": "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-use-callback-ref": "1.1.1"
},
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-layout-effect": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.1.tgz",
"integrity": "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-use-previous": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.1.tgz",
"integrity": "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==",
"license": "MIT",
"peerDependencies": {
"@types/react": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@radix-ui/react-visually-hidden": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.2.3.tgz",
"integrity": "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==",
"license": "MIT",
"dependencies": {
"@radix-ui/react-primitive": "2.1.3"
},
"peerDependencies": {
"@types/react": "*",
"@types/react-dom": "*",
"react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
"react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
}
}
},
"node_modules/@rtsao/scc": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz",
@ -11354,8 +11669,9 @@
"version": "19.2.2",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz",
"integrity": "sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==",
"dev": true,
"devOptional": true,
"license": "MIT",
"peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@ -12294,6 +12610,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/blake3-wasm": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/blake3-wasm/-/blake3-wasm-2.1.5.tgz",
@ -12747,6 +13072,15 @@
"node": ">= 8"
}
},
"node_modules/css-line-break": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
"integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
@ -14702,6 +15036,19 @@
"node": ">= 0.4"
}
},
"node_modules/html2canvas": {
"version": "1.4.1",
"resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
"integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
"license": "MIT",
"dependencies": {
"css-line-break": "^2.1.0",
"text-segmentation": "^1.0.3"
},
"engines": {
"node": ">=8.0.0"
}
},
"node_modules/http-errors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz",
@ -18202,6 +18549,15 @@
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"license": "MIT"
},
"node_modules/text-segmentation": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
"integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
"license": "MIT",
"dependencies": {
"utrie": "^1.0.2"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
@ -18544,6 +18900,15 @@
"node": ">= 0.4.0"
}
},
"node_modules/utrie": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
"integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
"license": "MIT",
"dependencies": {
"base64-arraybuffer": "^1.0.2"
}
},
"node_modules/uuid": {
"version": "9.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz",

View File

@ -13,10 +13,12 @@
},
"dependencies": {
"@opennextjs/cloudflare": "^1.3.0",
"@radix-ui/react-navigation-menu": "^1.2.14",
"@radix-ui/react-slot": "^1.2.3",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.23.24",
"html2canvas": "^1.4.1",
"lucide-react": "^0.552.0",
"minecraft-server-util": "^5.4.4",
"motion": "^12.23.24",

301
pnpm-lock.yaml generated
View File

@ -11,6 +11,9 @@ importers:
'@opennextjs/cloudflare':
specifier: ^1.3.0
version: 1.11.0(wrangler@4.45.3)
'@radix-ui/react-navigation-menu':
specifier: ^1.2.14
version: 1.2.14(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot':
specifier: ^1.2.3
version: 1.2.3(@types/react@19.2.2)(react@19.1.0)
@ -1092,6 +1095,22 @@ packages:
'@poppinss/exception@1.2.2':
resolution: {integrity: sha512-m7bpKCD4QMlFCjA/nKTs23fuvoVFoA83brRKmObCUNmi/9tVu8Ve3w4YQAnJu4q3Tjf5fr685HYIC/IA2zHRSg==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-collection@1.1.7':
resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
@ -1101,6 +1120,85 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.11':
resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-id@1.1.1':
resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-navigation-menu@1.2.14':
resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
@ -1110,6 +1208,73 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.2.2':
resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-effect-event@0.0.2':
resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-escape-keydown@1.1.1':
resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-previous@1.1.1':
resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-visually-hidden@1.2.3':
resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@rtsao/scc@1.1.0':
resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==}
@ -5380,12 +5545,99 @@ snapshots:
'@poppinss/exception@1.2.2': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-context@1.1.2(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-direction@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-id@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-context': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-direction': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-id': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@radix-ui/react-slot@1.2.3(@types/react@19.2.2)(react@19.1.0)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.2)(react@19.1.0)
@ -5393,6 +5645,55 @@ snapshots:
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.2)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.2)(react@19.1.0)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.2)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.2)(react@19.1.0)
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-use-previous@1.1.1(@types/react@19.2.2)(react@19.1.0)':
dependencies:
react: 19.1.0
optionalDependencies:
'@types/react': 19.2.2
'@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)':
dependencies:
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.2(@types/react@19.2.2))(@types/react@19.2.2)(react-dom@19.1.0(react@19.1.0))(react@19.1.0)
react: 19.1.0
react-dom: 19.1.0(react@19.1.0)
optionalDependencies:
'@types/react': 19.2.2
'@types/react-dom': 19.2.2(@types/react@19.2.2)
'@rtsao/scc@1.1.0': {}
'@rushstack/eslint-patch@1.14.1': {}

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

BIN
public/lever-down.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

BIN
public/lever-up.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -18,8 +18,6 @@ interface PlanPlayersResponse {
[key: string]: unknown;
}
export const runtime = 'edge';
// Extract player name from HTML string
function extractPlayerName(nameString: string): string {
// Name format: '<a class="link" href="../player/...">PlayerName</a>'
@ -32,9 +30,14 @@ export async function GET() {
const playersResponse = await fetch(
`${PLAN_BASE_URL}/v1/players?server=${SERVER_NAME}`,
{
next: { revalidate: 300 }, // Cache for 5 minutes
headers: {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (compatible; BiohazardVFX/1.0)',
'Referer': PLAN_BASE_URL,
},
cf: {
cacheTtl: 300,
cacheEverything: true,
}
}
);

View File

@ -25,9 +25,14 @@ export async function GET() {
const overviewResponse = await fetch(
`${PLAN_BASE_URL}/v1/serverOverview?server=${SERVER_NAME}`,
{
next: { revalidate: 30 }, // Cache for 30 seconds
headers: {
'Accept': 'application/json',
'User-Agent': 'Mozilla/5.0 (compatible; BiohazardVFX/1.0)',
'Referer': PLAN_BASE_URL,
},
cf: {
cacheTtl: 30,
cacheEverything: true,
}
}
);

View File

@ -6,9 +6,10 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: Oxanium, sans-serif;
--font-sans: var(--font-chivo-mono), Chivo Mono, monospace;
--font-mono: Fira Code, monospace;
--font-serif: Merriweather, serif;
--font-serif: var(--font-merriweather), Merriweather, serif;
--font-heading: var(--font-merriweather), Merriweather, serif;
--radius: 0.3rem;
--tracking-normal: 0em;
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
@ -189,12 +190,144 @@
* {
@apply border-border outline-ring/50;
}
html {
/* Cross-browser viewport height fix */
height: 100%;
height: -webkit-fill-available;
}
body {
@apply bg-background text-foreground;
font-family: var(--font-chivo-mono), Chivo Mono, monospace;
letter-spacing: var(--tracking-normal);
min-height: 100vh;
min-height: 100dvh;
min-height: -webkit-fill-available;
overflow-x: hidden;
/* Prevent horizontal scrollbar */
width: 100%;
max-width: 100vw;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-merriweather), Merriweather, serif;
}
h1 {
line-height: 1.1;
letter-spacing: -0.02em;
}
h2 {
line-height: 1.2;
letter-spacing: -0.01em;
}
h3 {
line-height: 1.3;
}
p, body {
line-height: 1.6;
}
/* Ensure consistent box-sizing */
*,
*::before,
*::after {
box-sizing: border-box;
}
}
/* Hide scrollbar but keep scroll functionality */
.hide-scrollbar {
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE and Edge */
}
.hide-scrollbar::-webkit-scrollbar {
display: none; /* Chrome, Safari, Opera */
}
/* Card depth utilities using theme shadow variables */
.card-depth-1 {
box-shadow: var(--shadow-lg), inset 0 1px 2px 0 rgba(0, 0, 0, 0.05);
}
.card-depth-2 {
box-shadow: var(--shadow-2xl), inset 0 2px 4px 0 rgba(0, 0, 0, 0.08);
}
/* Text shadow utilities using foreground with opacity */
.text-shadow-sm {
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
}
.text-shadow-md {
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
.text-shadow-lg {
text-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
/* Noise texture using SVG pattern */
.noise-texture {
position: relative;
}
.noise-texture::before {
content: '';
position: absolute;
inset: 0;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
pointer-events: none;
z-index: 1;
}
/* Typography scale utilities */
.text-heading-1 {
font-size: clamp(2.5rem, 8vw, 6rem);
line-height: 1.1;
letter-spacing: -0.02em;
}
.text-heading-2 {
font-size: clamp(1.875rem, 4vw, 3rem);
line-height: 1.2;
letter-spacing: -0.01em;
}
.text-heading-3 {
font-size: clamp(1.25rem, 2.5vw, 1.875rem);
line-height: 1.3;
}
/* Theme wipe overlay - shows old theme on left, reveals new theme on right */
.theme-wipe-overlay {
background: var(--background);
backgroundSize: cover;
backgroundPosition: center;
backgroundRepeat: no-repeat;
WebkitMaskImage: linear-gradient(to right, transparent 0%, transparent 0%, black 0%, black 100%);
maskImage: linear-gradient(to right, transparent 0%, transparent 0%, black 0%, black 100%);
WebkitMaskSize: 100% 100%;
maskSize: 100% 100%;
WebkitMaskRepeat: no-repeat;
maskRepeat: no-repeat;
transition: opacity 0.1s ease-out;
}
.theme-wipe-overlay.theme-wipe-active {
animation: theme-wipe 0.6s cubic-bezier(0.4, 0, 0.2, 1) forwards;
}
@keyframes theme-wipe {
0% {
WebkitMaskImage: linear-gradient(to right, transparent 0%, transparent 0%, black 0%, black 100%);
maskImage: linear-gradient(to right, transparent 0%, transparent 0%, black 0%, black 100%);
}
100% {
WebkitMaskImage: linear-gradient(to right, transparent 0%, transparent 100%, black 100%, black 100%);
maskImage: linear-gradient(to right, transparent 0%, transparent 100%, black 100%, black 100%);
}
}
/* Rocker Switch Styles */
.rocker {
display: inline-block;

View File

@ -1,8 +1,7 @@
import type { Metadata } from "next";
import { Geist, Geist_Mono } from "next/font/google";
import { Geist, Geist_Mono, Merriweather, Chivo_Mono } from "next/font/google";
import { Toaster } from 'sonner';
import { ThemeProvider } from "@/lib/theme-provider";
import { Navigation } from "@/components/navigation";
import { CustomFooter } from "@/components/custom-footer";
import "./globals.css";
@ -16,6 +15,17 @@ const geistMono = Geist_Mono({
subsets: ["latin"],
});
const merriweather = Merriweather({
variable: "--font-merriweather",
subsets: ["latin"],
weight: ["300", "400", "700", "900"],
});
const chivoMono = Chivo_Mono({
variable: "--font-chivo-mono",
subsets: ["latin"],
});
export const metadata: Metadata = {
title: {
default: "BiohazardVFX Minecraft Server | Join Our SMP Community",
@ -76,7 +86,7 @@ export default function RootLayout({
return (
<html lang="en" suppressHydrationWarning>
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
className={`${geistSans.variable} ${geistMono.variable} ${merriweather.variable} ${chivoMono.variable} antialiased`}
>
<ThemeProvider
attribute="class"
@ -84,7 +94,6 @@ export default function RootLayout({
enableSystem
disableTransitionOnChange
>
<Navigation />
{children}
<CustomFooter />
<Toaster position="bottom-right" />

View File

@ -9,6 +9,7 @@ import { Hero } from "@/components/hero";
import { ServerStatus } from "@/components/server-status";
import { Leaderboard } from "@/components/leaderboard";
import { MinecraftServerStructuredData } from "@/components/structured-data";
import { ThemeToggle } from "@/components/theme-toggle";
import { MessageCircle } from "lucide-react";
interface ServerData {
@ -65,43 +66,57 @@ export default function Home() {
return (
<>
<MinecraftServerStructuredData serverData={structuredDataProps} />
<div className="relative min-h-svh overflow-hidden">
<div className="relative min-h-screen min-h-svh overflow-hidden">
<div className="absolute inset-0">
{/* Light mode background */}
<Image
src="/backgroundimg.avif"
alt="Minecraft background"
fill
sizes="100vw"
className="object-cover"
className="object-cover dark:hidden"
priority
/>
{/* Dark mode background */}
<Image
src="/backgroundimg-dark.avif"
alt="Minecraft background dark"
fill
sizes="100vw"
className="hidden dark:block object-cover"
priority
/>
<div className="absolute inset-0 bg-gradient-to-br from-black/52 via-black/28 to-black/10 backdrop-blur-[1.5px]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.32),_transparent_60%)]" />
<div className="absolute inset-0 bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.32),_transparent_60%)] dark:bg-[radial-gradient(circle_at_top_left,_rgba(255,255,255,0.15),_transparent_60%)]" />
</div>
<div className="relative z-10 flex min-h-svh flex-col font-sans">
<main className="flex flex-1 items-center justify-center px-6 py-16 sm:px-10 lg:px-14">
<div className="w-full max-w-7xl">
<div className="flex gap-6 items-start relative">
<div className="relative z-10 flex min-h-screen min-h-svh flex-col font-sans overflow-x-hidden">
<main className="flex flex-1 items-center justify-center px-6 py-16 sm:px-10 lg:px-14 overflow-x-hidden">
<div className="w-full max-w-7xl mx-auto">
<div className="flex gap-6 items-stretch relative">
{/* Leaderboard - Left Side (separate card) */}
<section className="hidden lg:flex lg:flex-shrink-0 w-[280px] rounded-3xl border border-white/10 bg-background/96 p-4 shadow-2xl backdrop-blur-xl self-start overflow-visible" style={{ maxHeight: '650px' }}>
<section className="hidden lg:flex lg:flex-shrink-0 w-[300px] rounded-3xl border-2 border-white/10 bg-background/96 p-6 shadow-2xl backdrop-blur-xl backdrop-saturate-150 overflow-hidden card-depth-2 noise-texture" style={{ maxHeight: '650px' }}>
<Leaderboard className="h-full" />
</section>
{/* Main Card - Centered */}
<div className="flex-1 flex justify-center">
<section className="w-full max-w-5xl rounded-3xl border border-white/10 bg-background/96 p-8 shadow-2xl backdrop-blur-xl sm:p-10 lg:p-12" id="main-card">
<div className="grid gap-10 lg:grid-cols-[minmax(0,1.05fr)_minmax(0,0.95fr)]">
<div className="flex-1 flex justify-center min-w-0">
<section className="w-full max-w-6xl rounded-3xl border-2 border-white/10 bg-background/96 p-8 shadow-2xl backdrop-blur-xl backdrop-saturate-150 sm:p-10 lg:p-12 overflow-hidden relative card-depth-2 noise-texture" id="main-card">
{/* Theme Toggle - Top Right */}
<div className="absolute top-6 right-6 z-10">
<ThemeToggle />
</div>
<div className="grid gap-8 lg:grid-cols-[minmax(0,1fr)_minmax(0,1fr)]">
<Hero />
<div className="flex flex-col gap-6" id="server-status">
<div className="rounded-3xl border border-border/40 bg-background p-6 shadow-lg">
<div className="flex flex-col gap-1 sm:flex-row sm:items-end sm:justify-between text-foreground">
<div className="flex flex-col gap-6 border-t border-white/10 pt-6 lg:border-t-0 lg:pt-0 lg:border-l lg:pl-8" id="server-status">
<div className="rounded-3xl border border-border/40 bg-background p-6 shadow-lg backdrop-blur-sm card-depth-1 noise-texture">
<div className="flex flex-col gap-2 sm:flex-row sm:items-end sm:justify-between text-foreground mb-6">
<div>
<p className="text-xs uppercase tracking-[0.35em] text-muted-foreground">
Live snapshot
</p>
<h2 className="text-2xl font-semibold tracking-tight">
<h2 className="text-2xl font-semibold tracking-tight mt-1">
Server Pulse
</h2>
</div>
@ -109,13 +124,11 @@ export default function Home() {
Auto refresh 30s
</span>
</div>
<div className="mt-6">
<ServerStatus className="h-full" />
</div>
<ServerStatus className="h-full" />
</div>
<motion.div
className="rounded-3xl border border-border/50 bg-background/50 p-6 shadow-xl backdrop-blur"
className="rounded-3xl border border-border/50 bg-background/50 p-6 shadow-xl backdrop-blur backdrop-saturate-150 transition-all duration-200 ease-out hover:scale-[1.02] hover:border-primary/50 card-depth-1 noise-texture"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 1.2, ease: [0.33, 1, 0.68, 1] }}
@ -149,7 +162,7 @@ export default function Home() {
].map((item, index) => (
<motion.div
key={item.label}
className="group rounded-xl border border-border/50 bg-background/95 p-4 shadow-md transition-all hover:border-primary/30 hover:shadow-lg"
className="group rounded-xl border border-border/50 bg-background/95 p-4 shadow-md transition-all duration-200 ease-out hover:scale-[1.02] hover:border-primary/30 hover:shadow-lg"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 1.5 + index * 0.1, ease: [0.33, 1, 0.68, 1] }}
@ -172,7 +185,7 @@ export default function Home() {
>
<Button
size="lg"
className="mt-6 w-full font-bold shadow-lg transition-all hover:shadow-xl"
className="mt-6 w-full font-bold shadow-lg transition-all duration-200 ease-out hover:scale-[1.02] hover:shadow-xl hover:shadow-primary/20 active:scale-[0.98]"
asChild
>
<a
@ -194,7 +207,7 @@ export default function Home() {
</div>
{/* Leaderboard Section - Mobile */}
<section className="mt-10 lg:hidden rounded-3xl border border-white/10 bg-background/96 p-8 shadow-2xl backdrop-blur-xl sm:p-10 lg:p-12">
<section className="mt-12 lg:hidden rounded-3xl border-2 border-white/10 bg-background/96 p-8 shadow-2xl backdrop-blur-xl backdrop-saturate-150 sm:p-10 lg:p-12 card-depth-2 noise-texture">
<Leaderboard />
</section>
</div>

View File

@ -1,7 +1,5 @@
'use client';
import Link from 'next/link';
import StickyFooter from '@/components/ui/footer';
import { motion } from 'framer-motion';
import { MessageCircle } from 'lucide-react';
import { toast } from 'sonner';
@ -42,7 +40,7 @@ export function CustomFooter() {
try {
await navigator.clipboard.writeText('minecraft.biohazardvfx.com');
toast.success('Server IP copied to clipboard!');
} catch (error) {
} catch {
toast.error('Failed to copy IP address');
}
};
@ -66,9 +64,8 @@ export function CustomFooter() {
},
},
}}
className="bg-gradient-to-br from-card via-muted to-card/90 py-6 md:py-12 px-4 md:px-12 h-full w-full flex flex-col justify-between relative overflow-hidden"
className="bg-background/96 backdrop-blur-xl border-t border-white/10 py-8 sm:py-10 lg:py-12 px-6 sm:px-10 lg:px-14 h-full w-full flex flex-col justify-between relative overflow-hidden"
>
<div className="absolute inset-0 bg-gradient-to-t from-background/20 to-transparent pointer-events-none" />
{/* Navigation Section */}
<motion.div className="relative z-10">
@ -81,11 +78,11 @@ export function CustomFooter() {
transition={{ delay: 0.3 + index * 0.1, duration: 0.6 }}
className="flex flex-col gap-2"
>
<h3 className="mb-2 uppercase text-muted-foreground text-xs font-semibold tracking-wider border-b border-border pb-1 hover:text-foreground transition-colors duration-300">
<h3 className="mb-2 uppercase text-muted-foreground text-xs font-semibold tracking-[0.35em] border-b border-white/10 pb-1 hover:text-foreground transition-colors duration-300">
{section.title}
</h3>
{section.links.map((link, linkIndex) => {
if (link.external) {
if ('external' in link && link.external) {
return (
<motion.a
key={linkIndex}
@ -93,7 +90,7 @@ export function CustomFooter() {
target="_blank"
rel="noopener noreferrer"
whileHover={{ x: 8 }}
className="text-muted-foreground hover:text-foreground transition-colors duration-300 font-sans text-xs md:text-sm group relative"
className="text-muted-foreground hover:text-foreground transition-colors duration-300 text-xs md:text-sm group relative"
>
<span className="relative">
{link.text}
@ -106,13 +103,13 @@ export function CustomFooter() {
</span>
</motion.a>
);
} else if (link.isButton) {
} else if ('isButton' in link && link.isButton) {
return (
<motion.button
key={linkIndex}
onClick={handleCopyIP}
whileHover={{ x: 8 }}
className="text-muted-foreground hover:text-foreground transition-colors duration-300 font-sans text-xs md:text-sm text-left group relative"
className="text-muted-foreground hover:text-foreground transition-colors duration-300 text-xs md:text-sm text-left group relative"
>
<span className="relative">
{link.text}
@ -125,11 +122,11 @@ export function CustomFooter() {
</span>
</motion.button>
);
} else if (link.isStatic) {
} else if ('isStatic' in link && link.isStatic) {
return (
<span
key={linkIndex}
className="text-muted-foreground font-sans text-xs md:text-sm"
className="text-muted-foreground text-xs md:text-sm"
>
{link.text}
</span>
@ -140,7 +137,7 @@ export function CustomFooter() {
key={linkIndex}
href={link.href}
whileHover={{ x: 8 }}
className="text-muted-foreground hover:text-foreground transition-colors duration-300 font-sans text-xs md:text-sm group relative"
className="text-muted-foreground hover:text-foreground transition-colors duration-300 text-xs md:text-sm group relative"
>
<span className="relative">
{link.text}
@ -173,7 +170,7 @@ export function CustomFooter() {
animate={{ opacity: 1, x: 0 }}
transition={{ delay: 1, duration: 0.8 }}
whileHover={{ scale: 1.02 }}
className="text-[12vw] md:text-[10vw] lg:text-[8vw] xl:text-[6vw] leading-[0.8] font-serif bg-gradient-to-r from-foreground via-muted-foreground to-foreground/60 bg-clip-text text-transparent cursor-default"
className="text-[12vw] md:text-[10vw] lg:text-[8vw] xl:text-[6vw] leading-[0.8] text-foreground cursor-default"
>
{footerData.title}
</motion.h1>
@ -192,7 +189,7 @@ export function CustomFooter() {
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 1.4, duration: 0.5 }}
className="text-muted-foreground text-xs md:text-sm font-sans hover:text-foreground transition-colors duration-300"
className="text-muted-foreground text-xs md:text-sm hover:text-foreground transition-colors duration-300"
>
{footerData.subtitle}
</motion.p>
@ -219,10 +216,10 @@ export function CustomFooter() {
rel="noopener noreferrer"
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
className="inline-flex items-center gap-2 w-8 h-8 md:w-10 md:h-10 rounded-full bg-muted hover:bg-gradient-to-r hover:from-primary hover:to-secondary transition-colors duration-300 justify-center"
className="inline-flex items-center gap-2 w-8 h-8 md:w-10 md:h-10 rounded-full border border-white/10 bg-background/50 hover:bg-primary hover:border-primary/50 transition-all duration-300 justify-center backdrop-blur-sm"
aria-label="Join Discord"
>
<MessageCircle className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground group-hover:text-primary-foreground" />
<MessageCircle className="h-4 w-4 md:h-5 md:w-5 text-muted-foreground group-hover:text-primary-foreground transition-colors duration-300" />
</motion.a>
</motion.div>
</motion.div>

View File

@ -15,6 +15,7 @@ const screenshots = [
'/2025-07-13_12.59.13.png',
'/2025-07-13_13.01.28.png',
'/2025-10-22_17.07.42.png',
'/backgroundimg.avif',
];
export function Hero() {
@ -52,9 +53,9 @@ export function Hero() {
};
return (
<div className="w-full max-w-xl space-y-8">
<div className="w-full max-w-xl space-y-6 min-w-0">
<motion.div
className="flex flex-col gap-6 text-foreground"
className="flex flex-col gap-6 text-foreground min-w-0"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.1 }}
@ -68,24 +69,24 @@ export function Hero() {
Biohazard SMP
</motion.span>
<motion.h1
className="text-6xl font-bold leading-[1.1] tracking-tight sm:text-7xl lg:text-8xl"
className="text-6xl font-bold leading-[1.1] break-words overflow-wrap-anywhere sm:text-7xl lg:text-8xl"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.3 }}
>
<span className="block text-primary">
<span className="block text-primary break-words tracking-tighter text-shadow-md">
<HoverRollingText
text="Build."
transition={{ duration: 0.6, delay: 0.05 }}
/>
</span>
<span className="block text-secondary">
<span className="block text-secondary break-words tracking-normal text-shadow-sm">
<HoverRollingText
text="Explore."
transition={{ duration: 0.6, delay: 0.05 }}
/>
</span>
<span className="block text-accent">
<span className="block text-accent break-words tracking-normal text-shadow-sm">
<HoverRollingText
text="Survive."
transition={{ duration: 0.6, delay: 0.05 }}
@ -93,7 +94,7 @@ export function Hero() {
</span>
</motion.h1>
<motion.p
className="max-w-md text-base leading-relaxed text-muted-foreground sm:text-lg"
className="max-w-md text-base leading-relaxed text-muted-foreground sm:text-lg break-words"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.5 }}
@ -109,19 +110,19 @@ export function Hero() {
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.7 }}
>
<div className="group rounded-2xl border border-border/50 bg-card/50 p-6 shadow-xl backdrop-blur transition-all hover:border-primary/30 hover:shadow-2xl">
<div className="group rounded-2xl border border-border/50 bg-card/50 p-6 shadow-xl backdrop-blur backdrop-saturate-150 transition-all duration-200 ease-out hover:scale-[1.02] hover:border-primary/30 hover:shadow-2xl noise-texture">
<span className="text-xs font-bold uppercase tracking-[0.2em] text-muted-foreground">
Server IP
</span>
<div className="mt-4 flex items-center justify-between gap-4 rounded-xl border border-border/50 bg-background/90 px-5 py-4 shadow-sm">
<code className="text-base font-mono font-bold text-foreground sm:text-lg">
<div className="mt-4 flex items-center justify-between gap-4 rounded-xl border border-border/50 bg-background/90 px-5 py-4 shadow-sm min-w-0">
<code className="text-base font-mono font-bold text-foreground sm:text-lg break-all min-w-0">
{serverAddress}
</code>
<Button
onClick={copyServerAddress}
size="icon-sm"
variant="outline"
className="shrink-0 border-primary/30 bg-primary/10 hover:bg-primary/20 hover:border-primary/50 transition-all"
className="shrink-0 border-primary/30 bg-primary/10 transition-all duration-200 ease-out hover:bg-primary/20 hover:border-primary/50 hover:shadow-lg hover:shadow-primary/20 active:scale-[0.98]"
aria-label={copied ? 'Server address copied' : 'Copy server address'}
>
{copied ? <Check className="h-4 w-4 text-primary" /> : <Copy className="h-4 w-4 text-primary" />}
@ -132,7 +133,7 @@ export function Hero() {
{/* Screenshot Showcase */}
<motion.div
className="group relative aspect-video overflow-hidden rounded-3xl border border-border/50 bg-card/50 shadow-2xl backdrop-blur transition-all hover:shadow-3xl cursor-pointer"
className="group relative aspect-video overflow-hidden rounded-3xl border-2 border-white/10 bg-card/50 shadow-2xl backdrop-blur-xl backdrop-saturate-150 transition-all duration-200 ease-out hover:scale-[1.02] hover:border-primary/50 hover:shadow-2xl hover:shadow-primary/20 cursor-pointer noise-texture"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: 0.9, ease: [0.33, 1, 0.68, 1] }}

View File

@ -4,7 +4,7 @@ import { useEffect, useState } from 'react';
import { motion } from 'motion/react';
import { Skeleton } from '@/components/ui/skeleton';
import { cn } from '@/lib/utils';
import { Trophy, Clock, Calendar } from 'lucide-react';
import { Trophy, Clock } from 'lucide-react';
import { PlayerTooltip } from '@/components/player-tooltip';
interface LeaderboardPlayer {
@ -64,8 +64,8 @@ export function Leaderboard({ className }: { className?: string }) {
<Trophy className="h-4 w-4 text-primary" />
<h2 className="text-base font-semibold">Top Players</h2>
</div>
<div className="flex-1 overflow-y-auto" style={{ minHeight: 0 }}>
<div className="space-y-2 pr-2">
<div className="flex-1 overflow-y-auto hide-scrollbar" style={{ minHeight: 0 }}>
<div className="space-y-2">
{[1, 2, 3, 4, 5, 6].map((i) => (
<div key={i} className="flex items-center gap-2 p-2 rounded-lg border border-border/50">
<Skeleton className="h-5 w-5 rounded-full" />
@ -109,8 +109,8 @@ export function Leaderboard({ className }: { className?: string }) {
<Trophy className="h-4 w-4 text-primary" />
<h2 className="text-base font-semibold">Top Players</h2>
</div>
<div className="flex-1 overflow-y-auto" style={{ minHeight: 0 }}>
<div className="space-y-1.5 pr-2" style={{ paddingBottom: '2.5rem' }}>
<div className="flex-1 overflow-y-auto hide-scrollbar" style={{ minHeight: 0 }}>
<div className="space-y-1.5" style={{ paddingBottom: '2.5rem' }}>
{data.players.slice(0, 10).map((player, index) => {
const rank = index + 1;
const isTopThree = rank <= 3;
@ -119,21 +119,21 @@ export function Leaderboard({ className }: { className?: string }) {
<motion.div
key={player.name}
className={cn(
"flex items-center gap-2 p-2 rounded-lg border transition-all text-sm",
"flex items-center gap-2 p-2 rounded-lg border transition-all duration-200 ease-out text-sm",
isTopThree
? "bg-primary/5 border-primary/20 shadow-sm"
: "bg-background/50 border-border/50 hover:border-primary/30"
? "bg-primary/5 border-primary/20 shadow-sm hover:scale-[1.02] hover:border-primary/30"
: "bg-background/50 border-border/50 hover:scale-[1.02] hover:border-primary/30"
)}
initial={{ opacity: 0, x: -20 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.2, delay: index * 0.03 }}
transition={{ duration: 0.2, delay: index * 0.03, ease: [0.4, 0, 0.2, 1] }}
>
<div className={cn(
"flex items-center justify-center w-6 h-6 rounded-full font-bold text-xs flex-shrink-0",
rank === 1 && "bg-primary text-primary-foreground",
rank === 2 && "bg-secondary text-secondary-foreground",
rank === 3 && "bg-accent text-accent-foreground",
rank > 3 && "bg-muted text-muted-foreground"
"flex items-center justify-center w-6 h-6 rounded-full font-bold text-xs flex-shrink-0 border transition-all duration-200",
rank === 1 && "bg-primary/10 border-primary/30 text-primary",
rank === 2 && "bg-primary/10 border-primary/30 text-primary",
rank === 3 && "bg-primary/10 border-primary/30 text-primary",
rank > 3 && "bg-muted text-muted-foreground border-border/50"
)}>
{rank <= 3 ? getRankBadge(rank) : rank}
</div>

View File

@ -1,45 +1,151 @@
'use client';
import { useState } from 'react';
import Link from 'next/link';
import { Menu, MenuItem, HoveredLink } from '@/components/ui/navbar-menu';
import {
NavigationMenu,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
navigationMenuTriggerStyle,
} from '@/components/ui/navigation-menu';
import { ThemeToggle } from '@/components/theme-toggle';
import { MessageCircle } from 'lucide-react';
import { MessageCircle, Menu as MenuIcon, X } from 'lucide-react';
import { cn } from '@/lib/utils';
import { useState, useEffect } from 'react';
import { usePathname } from 'next/navigation';
interface NavLink {
label: string;
href: string;
external?: boolean;
icon?: React.ReactNode;
}
const navItems: NavLink[] = [
{
label: 'Home',
href: '/',
},
{
label: 'Server',
href: '/#server-status',
},
{
label: 'Community',
href: 'https://discord.gg/58FnVzmzrS',
external: true,
icon: <MessageCircle className="h-4 w-4" />,
},
];
export function Navigation() {
const [active, setActive] = useState<string | null>(null);
const [isMenuOpen, setIsMenuOpen] = useState(false);
const pathname = usePathname();
// Close mobile menu on route change
useEffect(() => {
setIsMenuOpen(false);
}, [pathname]);
const isActive = (href: string) => {
if (href.startsWith('/#')) {
return pathname === '/';
}
return pathname === href;
};
const handleNavClick = (e: React.MouseEvent, href: string) => {
if (href.startsWith('/#')) {
e.preventDefault();
const id = href.replace('/#', '');
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'start' });
}
}
};
return (
<div className="fixed top-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-6xl px-4">
<Menu setActive={setActive}>
<MenuItem setActive={setActive} active={active} item="Home">
<div className="flex flex-col space-y-4 text-sm">
<HoveredLink href="/">Home</HoveredLink>
<>
{/* Desktop Navigation */}
<nav className="hidden md:flex fixed top-4 left-1/2 -translate-x-1/2 z-50 justify-center items-center overflow-x-hidden">
<NavigationMenu className="px-2 py-2 overflow-hidden">
<NavigationMenuList className="gap-2 flex-wrap">
{navItems.map((item) => (
<NavigationMenuItem key={item.label}>
<NavigationMenuLink asChild>
<Link
href={item.href}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
onClick={(e) => handleNavClick(e, item.href)}
className={cn(
navigationMenuTriggerStyle(),
'text-foreground hover:text-primary hover:bg-accent focus:bg-accent focus:text-primary whitespace-nowrap',
isActive(item.href) && 'bg-accent/50 text-accent-foreground'
)}
>
<span className="flex items-center gap-2">
{item.icon}
{item.label}
</span>
</Link>
</NavigationMenuLink>
</NavigationMenuItem>
))}
{/* Theme Toggle */}
<NavigationMenuItem>
<div className="px-2 flex-shrink-0">
<ThemeToggle />
</div>
</NavigationMenuItem>
</NavigationMenuList>
</NavigationMenu>
</nav>
{/* Mobile Navigation */}
<div className="md:hidden fixed top-4 right-4 z-50">
<button
onClick={() => setIsMenuOpen(!isMenuOpen)}
className="rounded-full border border-border/50 bg-background/95 backdrop-blur-md shadow-lg p-3"
aria-label="Toggle navigation menu"
aria-expanded={isMenuOpen}
>
{isMenuOpen ? (
<X className="h-5 w-5 text-foreground" />
) : (
<MenuIcon className="h-5 w-5 text-foreground" />
)}
</button>
{/* Mobile Menu Panel */}
{isMenuOpen && (
<div className="absolute right-0 mt-3 w-64 max-w-[calc(100vw-2rem)] rounded-2xl border border-border/50 bg-background/95 backdrop-blur-md shadow-lg p-4 overflow-hidden">
<nav className="flex flex-col space-y-2">
{navItems.map((item) => (
<Link
key={item.label}
href={item.href}
target={item.external ? '_blank' : undefined}
rel={item.external ? 'noopener noreferrer' : undefined}
onClick={(e) => handleNavClick(e, item.href)}
className={cn(
'flex items-center gap-2 px-4 py-3 rounded-lg text-foreground hover:text-primary hover:bg-accent transition-colors break-words',
isActive(item.href) && 'bg-accent/50 text-accent-foreground'
)}
>
{item.icon}
{item.label}
</Link>
))}
<div className="px-4 py-3">
<ThemeToggle />
</div>
</nav>
</div>
</MenuItem>
<MenuItem setActive={setActive} active={active} item="Server">
<div className="flex flex-col space-y-4 text-sm">
<HoveredLink href="/#server-status">Server Status</HoveredLink>
</div>
</MenuItem>
<MenuItem setActive={setActive} active={active} item="Community">
<div className="flex flex-col space-y-4 text-sm">
<HoveredLink
href="https://discord.gg/58FnVzmzrS"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2"
>
<MessageCircle className="h-4 w-4" />
Discord
</HoveredLink>
</div>
</MenuItem>
<div className="flex items-center justify-center">
<ThemeToggle />
</div>
</Menu>
</div>
)}
</div>
</>
);
}

View File

@ -223,7 +223,7 @@ export function ServerStatus({ className }: ServerStatusProps = {}) {
return (
<motion.div
className={cn(
"space-y-6 rounded-2xl border border-border/50 bg-card/50 p-6 text-card-foreground shadow-xl backdrop-blur",
"space-y-5 text-card-foreground",
className
)}
initial={{ opacity: 0, x: 20 }}
@ -239,15 +239,10 @@ export function ServerStatus({ className }: ServerStatusProps = {}) {
>
<div className="flex items-center gap-2.5">
<motion.span
className="h-3 w-3 rounded-full bg-emerald-500 shadow-lg"
className="h-3 w-3 rounded-full bg-primary shadow-lg"
animate={{
scale: [1, 1.2, 1],
opacity: [1, 0.8, 1],
boxShadow: [
'0 0 0px rgba(16, 185, 129, 0.4)',
'0 0 10px rgba(16, 185, 129, 0.8)',
'0 0 0px rgba(16, 185, 129, 0.4)',
],
}}
transition={{
duration: 2,
@ -256,7 +251,7 @@ export function ServerStatus({ className }: ServerStatusProps = {}) {
}}
/>
<motion.span
className="text-sm font-bold text-emerald-600 dark:text-emerald-400"
className="text-sm font-bold text-primary"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ delay: 0.6 }}
@ -270,7 +265,7 @@ export function ServerStatus({ className }: ServerStatusProps = {}) {
</motion.div>
<motion.dl
className="grid min-w-0 gap-3 sm:grid-cols-2"
className="grid min-w-0 gap-4 sm:grid-cols-2"
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.4, delay: 0.6, ease: [0.33, 1, 0.68, 1] }}
@ -279,20 +274,25 @@ export function ServerStatus({ className }: ServerStatusProps = {}) {
{statTiles.map((tile, index) => (
<motion.div
key={tile.label}
className="group min-w-0 overflow-hidden rounded-xl border border-border/50 bg-background/95 p-4 shadow-md transition-all hover:border-primary/30 hover:shadow-lg"
className="group min-w-0 overflow-hidden rounded-xl border border-border/50 bg-background/95 p-5 shadow-md backdrop-blur-sm transition-all duration-200 ease-out hover:scale-[1.02] hover:border-primary/30 hover:shadow-lg noise-texture"
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.4, delay: 0.7 + index * 0.1, ease: [0.33, 1, 0.68, 1] }}
style={{ backfaceVisibility: "hidden", WebkitBackfaceVisibility: "hidden", transform: "translateZ(0)" }}
>
<dt className="truncate text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">
<dt className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground break-words">
{tile.label}
</dt>
<dd className="mt-2 truncate text-2xl font-bold text-foreground">
<dd className={cn(
"mt-3 text-2xl font-bold break-words break-all",
(tile.label === 'Players Online' || tile.label === 'Total Players')
? "text-primary"
: "text-foreground"
)}>
{tile.value}
</dd>
{tile.hint ? (
<p className="mt-1 truncate text-[11px] font-medium uppercase tracking-[0.05em] text-muted-foreground/70">
<p className="mt-2 text-[11px] font-medium uppercase tracking-[0.05em] text-muted-foreground/70 break-words">
{tile.hint}
</p>
) : null}
@ -319,9 +319,9 @@ export function ServerStatus({ className }: ServerStatusProps = {}) {
)}
{status.stats?.averageTps ? (
<div className="flex items-center justify-between rounded-xl border border-border/50 bg-background/95 px-4 py-3.5 shadow-md">
<div className="flex items-center justify-between rounded-xl border border-border/50 bg-background/95 px-5 py-4 shadow-md backdrop-blur-sm transition-all duration-200 ease-out hover:scale-[1.02] hover:border-primary/30 hover:shadow-lg noise-texture">
<span className="text-xs font-semibold uppercase tracking-[0.08em] text-muted-foreground">Average TPS (7d)</span>
<span className="text-xl font-bold text-foreground">
<span className="text-2xl font-bold text-foreground">
{status.stats.averageTps}
</span>
</div>

View File

@ -2,7 +2,7 @@
import * as React from 'react';
import { useTheme } from 'next-themes';
import { Moon, Sun } from 'lucide-react';
import Image from 'next/image';
import { cn } from '@/lib/utils';
export function ThemeToggle({ className }: { className?: string }) {
@ -14,25 +14,32 @@ export function ThemeToggle({ className }: { className?: string }) {
}, []);
if (!mounted) {
return <div className={cn('h-9 w-16', className)} />;
return <div className={cn('h-16 w-16', className)} />;
}
const isDark = theme === 'dark';
const toggleTheme = () => {
setTheme(isDark ? 'light' : 'dark');
};
return (
<label className={cn("rocker rocker-small", className)}>
<input
type="checkbox"
checked={isDark}
onChange={(e) => setTheme(e.target.checked ? 'dark' : 'light')}
aria-label="Toggle theme"
<button
onClick={toggleTheme}
className={cn(
"relative w-16 h-16 transition-transform hover:scale-110 active:scale-95 cursor-pointer",
className
)}
aria-label="Toggle theme"
>
<Image
src={isDark ? "/lever-down.png" : "/lever-up.png"}
alt={isDark ? "Dark mode lever" : "Light mode lever"}
width={64}
height={64}
className="object-contain"
priority
/>
<span className="switch-left">
<Sun className="h-3 w-3" />
</span>
<span className="switch-right">
<Moon className="h-3 w-3" />
</span>
</label>
</button>
);
}

View File

@ -0,0 +1,61 @@
'use client';
import { useEffect, useState, useRef } from 'react';
import { useTheme } from 'next-themes';
export function ThemeTransition() {
const { theme, resolvedTheme } = useTheme();
const [prevTheme, setPrevTheme] = useState<string | undefined>();
const [capturedImage, setCapturedImage] = useState<string | null>(null);
const overlayRef = useRef<HTMLDivElement>(null);
useEffect(() => {
// Listen for theme transition start event with captured image
const handleTransitionStart = (event: CustomEvent<{ imageDataUrl: string; newTheme: string }>) => {
setCapturedImage(event.detail.imageDataUrl);
};
window.addEventListener('theme-transition-start', handleTransitionStart as EventListener);
return () => {
window.removeEventListener('theme-transition-start', handleTransitionStart as EventListener);
};
}, []);
useEffect(() => {
const currentTheme = resolvedTheme || theme;
if (prevTheme && prevTheme !== currentTheme && currentTheme && overlayRef.current && capturedImage) {
// Set the captured image as background
overlayRef.current.style.backgroundImage = `url(${capturedImage})`;
overlayRef.current.style.backgroundSize = 'cover';
overlayRef.current.style.backgroundPosition = 'center';
overlayRef.current.style.opacity = '1';
overlayRef.current.classList.add('theme-wipe-active');
const timer = setTimeout(() => {
if (overlayRef.current) {
overlayRef.current.classList.remove('theme-wipe-active');
overlayRef.current.style.opacity = '0';
overlayRef.current.style.backgroundImage = '';
setCapturedImage(null);
}
}, 600);
return () => {
clearTimeout(timer);
};
}
setPrevTheme(currentTheme);
}, [theme, resolvedTheme, prevTheme, capturedImage]);
return (
<div
ref={overlayRef}
className="fixed inset-0 z-[9999] pointer-events-none theme-wipe-overlay"
style={{
opacity: 0,
}}
/>
);
}

View File

@ -5,11 +5,11 @@ import { cva, type VariantProps } from "class-variance-authority"
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",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all duration-200 ease-out 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",
{
variants: {
variant: {
default: "bg-primary text-primary-foreground hover:bg-primary/90",
default: "bg-primary text-primary-foreground hover:bg-primary/90 hover:shadow-lg hover:shadow-primary/20 active:scale-[0.98]",
destructive:
"bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60",
outline:

View File

@ -9,7 +9,6 @@ const containerVariants = {
y: 0,
transition: {
duration: 0.8,
ease: "easeOut",
staggerChildren: 0.1,
},
},
@ -20,7 +19,7 @@ const itemVariants = {
visible: {
opacity: 1,
x: 0,
transition: { duration: 0.6, ease: "easeOut" },
transition: { duration: 0.6 },
},
}
@ -29,7 +28,7 @@ const linkVariants = {
visible: {
opacity: 1,
y: 0,
transition: { duration: 0.4, ease: "easeOut" },
transition: { duration: 0.4 },
},
}
@ -39,7 +38,7 @@ const socialVariants = {
opacity: 1,
scale: 1,
transition: {
type: "spring",
type: "spring" as const,
stiffness: 200,
damping: 10,
},
@ -53,7 +52,6 @@ const backgroundVariants = {
scale: 1,
transition: {
duration: 2,
ease: "easeOut",
},
},
}

View File

@ -5,7 +5,7 @@ import Link from "next/link";
import Image from "next/image";
const transition = {
type: "spring",
type: "spring" as const,
mass: 0.5,
damping: 11.5,
stiffness: 100,
@ -109,7 +109,7 @@ export const ProductItem = ({
);
};
export const HoveredLink = ({ children, ...rest }: any) => {
export const HoveredLink = ({ children, ...rest }: React.ComponentProps<typeof Link>) => {
return (
<Link
{...rest}

View File

@ -0,0 +1,168 @@
import * as React from "react"
import * as NavigationMenuPrimitive from "@radix-ui/react-navigation-menu"
import { cva } from "class-variance-authority"
import { ChevronDownIcon } from "lucide-react"
import { cn } from "@/lib/utils"
function NavigationMenu({
className,
children,
viewport = true,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Root> & {
viewport?: boolean
}) {
return (
<NavigationMenuPrimitive.Root
data-slot="navigation-menu"
data-viewport={viewport}
className={cn(
"group/navigation-menu relative flex max-w-max flex-1 items-center justify-center",
className
)}
{...props}
>
{children}
{viewport && <NavigationMenuViewport />}
</NavigationMenuPrimitive.Root>
)
}
function NavigationMenuList({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.List>) {
return (
<NavigationMenuPrimitive.List
data-slot="navigation-menu-list"
className={cn(
"group flex flex-1 list-none items-center justify-center gap-1",
className
)}
{...props}
/>
)
}
function NavigationMenuItem({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Item>) {
return (
<NavigationMenuPrimitive.Item
data-slot="navigation-menu-item"
className={cn("relative", className)}
{...props}
/>
)
}
const navigationMenuTriggerStyle = cva(
"group inline-flex h-9 w-max items-center justify-center rounded-md bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground disabled:pointer-events-none disabled:opacity-50 data-[state=open]:hover:bg-accent data-[state=open]:text-accent-foreground data-[state=open]:focus:bg-accent data-[state=open]:bg-accent/50 focus-visible:ring-ring/50 outline-none transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1"
)
function NavigationMenuTrigger({
className,
children,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Trigger>) {
return (
<NavigationMenuPrimitive.Trigger
data-slot="navigation-menu-trigger"
className={cn(navigationMenuTriggerStyle(), "group", className)}
{...props}
>
{children}{" "}
<ChevronDownIcon
className="relative top-[1px] ml-1 size-3 transition duration-300 group-data-[state=open]:rotate-180"
aria-hidden="true"
/>
</NavigationMenuPrimitive.Trigger>
)
}
function NavigationMenuContent({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Content>) {
return (
<NavigationMenuPrimitive.Content
data-slot="navigation-menu-content"
className={cn(
"data-[motion^=from-]:animate-in data-[motion^=to-]:animate-out data-[motion^=from-]:fade-in data-[motion^=to-]:fade-out data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 top-0 left-0 w-full p-2 pr-2.5 md:absolute md:w-auto",
"group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:data-[state=open]:animate-in group-data-[viewport=false]/navigation-menu:data-[state=closed]:animate-out group-data-[viewport=false]/navigation-menu:data-[state=closed]:zoom-out-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-[state=open]:fade-in-0 group-data-[viewport=false]/navigation-menu:data-[state=closed]:fade-out-0 group-data-[viewport=false]/navigation-menu:top-full group-data-[viewport=false]/navigation-menu:mt-1.5 group-data-[viewport=false]/navigation-menu:overflow-hidden group-data-[viewport=false]/navigation-menu:rounded-md group-data-[viewport=false]/navigation-menu:border group-data-[viewport=false]/navigation-menu:shadow group-data-[viewport=false]/navigation-menu:duration-200 **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none",
className
)}
{...props}
/>
)
}
function NavigationMenuViewport({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Viewport>) {
return (
<div
className={cn(
"absolute top-full left-0 isolate z-50 flex justify-center"
)}
>
<NavigationMenuPrimitive.Viewport
data-slot="navigation-menu-viewport"
className={cn(
"origin-top-center bg-popover text-popover-foreground data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-90 relative mt-1.5 h-[var(--radix-navigation-menu-viewport-height)] w-full overflow-hidden rounded-md border shadow md:w-[var(--radix-navigation-menu-viewport-width)]",
className
)}
{...props}
/>
</div>
)
}
function NavigationMenuLink({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Link>) {
return (
<NavigationMenuPrimitive.Link
data-slot="navigation-menu-link"
className={cn(
"data-[active=true]:focus:bg-accent data-[active=true]:hover:bg-accent data-[active=true]:bg-accent/50 data-[active=true]:text-accent-foreground hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground focus-visible:ring-ring/50 [&_svg:not([class*='text-'])]:text-muted-foreground flex flex-col gap-1 rounded-sm p-2 text-sm transition-all outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function NavigationMenuIndicator({
className,
...props
}: React.ComponentProps<typeof NavigationMenuPrimitive.Indicator>) {
return (
<NavigationMenuPrimitive.Indicator
data-slot="navigation-menu-indicator"
className={cn(
"data-[state=visible]:animate-in data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:fade-in top-full z-[1] flex h-1.5 items-end justify-center overflow-hidden",
className
)}
{...props}
>
<div className="bg-border relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm shadow-md" />
</NavigationMenuPrimitive.Indicator>
)
}
export {
NavigationMenu,
NavigationMenuList,
NavigationMenuItem,
NavigationMenuContent,
NavigationMenuTrigger,
NavigationMenuLink,
NavigationMenuIndicator,
NavigationMenuViewport,
navigationMenuTriggerStyle,
}

View File

@ -35,20 +35,21 @@ export const AnimatedTooltip = ({ items }: AnimatedTooltipProps) => {
springConfig,
);
const handleMouseMove = (event: any) => {
const handleMouseMove = (event: React.MouseEvent<HTMLDivElement>) => {
if (animationFrameRef.current) {
cancelAnimationFrame(animationFrameRef.current);
}
animationFrameRef.current = requestAnimationFrame(() => {
const halfWidth = event.target.offsetWidth / 2;
const target = event.target as HTMLElement;
const halfWidth = target.offsetWidth / 2;
x.set(event.nativeEvent.offsetX - halfWidth);
});
};
return (
<>
{items.map((item, idx) => (
{items.map((item) => (
<div
className="group relative -mr-4"
key={item.name}

View File

@ -1,5 +1,4 @@
import { cn } from "@/lib/utils";
import { useState } from "react";
export const Component = () => {
return (

31
src/config/navigation.ts Normal file
View File

@ -0,0 +1,31 @@
export interface NavItem {
label: string;
href: string;
external?: boolean;
icon?: React.ReactNode;
}
export interface NavDropdown {
label: string;
items: Array<{
title: string;
description?: string;
href: string;
}>;
}
export const navigationItems: Array<NavItem | NavDropdown> = [
{
label: "Home",
href: "/",
},
{
label: "Server",
href: "/#server-status",
},
{
label: "Community",
href: "https://discord.gg/invite",
external: true,
},
];

View File

@ -21,6 +21,8 @@
"zone_name": "biohazardvfx.com"
}
],
"workers_dev": true,
"preview_urls": true,
"observability": {
"enabled": true
}