Compare commits

...
10 Commits
Author SHA1 Message Date
selimaj-dev 0812dae8c5 Fixed deploy issue with next.js vurnlability 2025-12-09 00:22:31 +01:00
selimaj-dev 57b26f1803 #5 Fix metadata 2025-12-06 23:06:00 +01:00
selimaj-dev 73c0c28f8a Nav menu for mobile 2025-12-06 22:45:18 +01:00
Klesti Selimaj 97761b5df4 Merge pull request #3 from orus-dev/next-critical-blog
Next.js critical (blog)
2025-12-04 00:14:31 +01:00
selimaj-dev 1446b46190 Next.js critical (blog) 2025-12-04 00:13:27 +01:00
Klesti Selimaj 5e68b9868c Merge pull request #2 from orus-dev/tiny-fixes
Tiny fixes
2025-12-03 23:21:38 +01:00
selimaj-dev 12ddf4db4b copy blog to clipboard 2025-12-03 23:20:40 +01:00
selimaj-dev 8fb7391d60 Fixed src in web stuff and added link to article title 2025-12-03 23:07:25 +01:00
Klesti Selimaj 73ee207912 Merge pull request #1 from orus-dev/blog-and-projects
Blog and projects
2025-12-03 21:43:27 +01:00
selimaj-dev 5c9b9ccb17 Blog and projects animation 2025-12-03 20:45:19 +01:00
14 changed files with 283 additions and 123 deletions
+11
View File
@@ -0,0 +1,11 @@
{
"title": "A critical vulnerability with Next.js 16: Code execution",
"description": "A critical vulnerability has been identified in the React Server Components (RSC) protocol",
"author": "Klesti Selimaj",
"date": "2025-12-03",
"readingTime": 1,
"tags": ["next.js", "vulnerability", "critical", "0day", "ssr"],
"image": "/blog-img/nextjs-critical.png",
"category": "news"
}
+32
View File
@@ -0,0 +1,32 @@
A critical vulnerability has been identified in the React Server Components (RSC) protocol. The issue is rated CVSS 10.0 and can allow remote code execution when processing attacker-controlled requests in unpatched environments.
<br/>
The exploit is running code on the server from the client without admin rights, somewhere in the SSR (Server Side Renderer) there is a exploit that allows a client to get unauthorized access to the server running Next.js.
<br/>
<br/>
Next.js is a react framework for making full-stack application with node.js and React.js, it has a complex SSR (Server Side Renderer) which makes it easy to miss things sometimes, however this is a very critical error, i believe it wasn't the staff's fault, things like these are very likely to happen, but Next.js is used by 75% of modern websites.
<br/>
## Affected versions
- Next.js 15.x
- Next.js 16.x
- Next.js 14.3.0-canary.77 and later canary releases
Next.js 13.x, Next.js 14.x stable, Pages Router applications, and the Edge Runtime are not affected.
## Fixed Versions
- 15.0.5
- 15.1.9
- 15.2.6
- 15.3.6
- 15.4.8
- 15.5.7
- 16.0.7
## Conclusion
They have fixed it as seen on the original blog post: https://nextjs.org/blog/CVE-2025-66478
But this shows just how important it is to watch out when working on such complex structures, one small mistake can cost a big price, remote code execution is one of them.
+40 -40
View File
@@ -12,7 +12,7 @@
"highlight.js": "^11.11.1",
"katex": "^0.16.25",
"lucide-react": "^0.555.0",
"next": "16.0.6",
"next": "^16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-markdown": "^10.1.0",
@@ -574,15 +574,15 @@
}
},
"node_modules/@next/env": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.6.tgz",
"integrity": "sha512-PFTK/G/vM3UJwK5XDYMFOqt8QW42mmhSgdKDapOlCqBUAOfJN2dyOnASR/xUR/JRrro0pLohh/zOJ77xUQWQAg==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/env/-/env-16.0.7.tgz",
"integrity": "sha512-gpaNgUh5nftFKRkRQGnVi5dpcYSKGcZZkQffZ172OrG/XkrnS7UBTQ648YY+8ME92cC4IojpI2LqTC8sTDhAaw==",
"license": "MIT"
},
"node_modules/@next/swc-darwin-arm64": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.6.tgz",
"integrity": "sha512-AGzKiPlDiui+9JcPRHLI4V9WFTTcKukhJTfK9qu3e0tz+Y/88B7vo5yZoO7UaikplJEHORzG3QaBFQfkjhnL0Q==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.0.7.tgz",
"integrity": "sha512-LlDtCYOEj/rfSnEn/Idi+j1QKHxY9BJFmxx7108A6D8K0SB+bNgfYQATPk/4LqOl4C0Wo3LACg2ie6s7xqMpJg==",
"cpu": [
"arm64"
],
@@ -596,9 +596,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.6.tgz",
"integrity": "sha512-LlLLNrK9WCIUkq2GciWDcquXYIf7vLxX8XE49gz7EncssZGL1vlHwgmURiJsUZAvk0HM1a8qb1ABDezsjAE/jw==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.0.7.tgz",
"integrity": "sha512-rtZ7BhnVvO1ICf3QzfW9H3aPz7GhBrnSIMZyr4Qy6boXF0b5E3QLs+cvJmg3PsTCG2M1PBoC+DANUi4wCOKXpA==",
"cpu": [
"x64"
],
@@ -612,9 +612,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.6.tgz",
"integrity": "sha512-r04NzmLSGGfG8EPXKVK72N5zDNnq9pa9el78LhdtqIC3zqKh74QfKHnk24DoK4PEs6eY7sIK/CnNpt30oc59kg==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.0.7.tgz",
"integrity": "sha512-mloD5WcPIeIeeZqAIP5c2kdaTa6StwP4/2EGy1mUw8HiexSHGK/jcM7lFuS3u3i2zn+xH9+wXJs6njO7VrAqww==",
"cpu": [
"arm64"
],
@@ -628,9 +628,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.6.tgz",
"integrity": "sha512-hfB/QV0hA7lbD1OJxp52wVDlpffUMfyxUB5ysZbb/pBC5iuhyLcEKSVQo56PFUUmUQzbMsAtUu6k2Gh9bBtWXA==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.0.7.tgz",
"integrity": "sha512-+ksWNrZrthisXuo9gd1XnjHRowCbMtl/YgMpbRvFeDEqEBd523YHPWpBuDjomod88U8Xliw5DHhekBC3EOOd9g==",
"cpu": [
"arm64"
],
@@ -644,9 +644,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.6.tgz",
"integrity": "sha512-PZJushBgfvKhJBy01yXMdgL+l5XKr7uSn5jhOQXQXiH3iPT2M9iG64yHpPNGIKitKrHJInwmhPVGogZBAJOCPw==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.0.7.tgz",
"integrity": "sha512-4WtJU5cRDxpEE44Ana2Xro1284hnyVpBb62lIpU5k85D8xXxatT+rXxBgPkc7C1XwkZMWpK5rXLXTh9PFipWsA==",
"cpu": [
"x64"
],
@@ -660,9 +660,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.6.tgz",
"integrity": "sha512-LqY76IojrH9yS5fyATjLzlOIOgwyzBuNRqXwVxcGfZ58DWNQSyfnLGlfF6shAEqjwlDNLh4Z+P0rnOI87Y9jEw==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.0.7.tgz",
"integrity": "sha512-HYlhqIP6kBPXalW2dbMTSuB4+8fe+j9juyxwfMwCe9kQPPeiyFn7NMjNfoFOfJ2eXkeQsoUGXg+O2SE3m4Qg2w==",
"cpu": [
"x64"
],
@@ -676,9 +676,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.6.tgz",
"integrity": "sha512-eIfSNNqAkj0tqKRf0u7BVjqylJCuabSrxnpSENY3YKApqwDMeAqYPmnOwmVe6DDl3Lvkbe7cJAyP6i9hQ5PmmQ==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.0.7.tgz",
"integrity": "sha512-EviG+43iOoBRZg9deGauXExjRphhuYmIOJ12b9sAPy0eQ6iwcPxfED2asb/s2/yiLYOdm37kPaiZu8uXSYPs0Q==",
"cpu": [
"arm64"
],
@@ -692,9 +692,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.6.tgz",
"integrity": "sha512-QGs18P4OKdK9y2F3Th42+KGnwsc2iaThOe6jxQgP62kslUU4W+g6AzI6bdIn/pslhSfxjAMU5SjakfT5Fyo/xA==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.0.7.tgz",
"integrity": "sha512-gniPjy55zp5Eg0896qSrf3yB1dw4F/3s8VK1ephdsZZ129j2n6e1WqCbE2YgcKhW9hPB9TVZENugquWJD5x0ug==",
"cpu": [
"x64"
],
@@ -2954,12 +2954,12 @@
}
},
"node_modules/next": {
"version": "16.0.6",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.6.tgz",
"integrity": "sha512-2zOZ/4FdaAp5hfCU/RnzARlZzBsjaTZ/XjNQmuyYLluAPM7kcrbIkdeO2SL0Ysd1vnrSgU+GwugfeWX1cUCgCg==",
"version": "16.0.7",
"resolved": "https://registry.npmjs.org/next/-/next-16.0.7.tgz",
"integrity": "sha512-3mBRJyPxT4LOxAJI6IsXeFtKfiJUbjCLgvXO02fV8Wy/lIhPvP94Fe7dGhUgHXcQy4sSuYwQNcOLhIfOm0rL0A==",
"license": "MIT",
"dependencies": {
"@next/env": "16.0.6",
"@next/env": "16.0.7",
"@swc/helpers": "0.5.15",
"caniuse-lite": "^1.0.30001579",
"postcss": "8.4.31",
@@ -2972,14 +2972,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
"@next/swc-darwin-arm64": "16.0.6",
"@next/swc-darwin-x64": "16.0.6",
"@next/swc-linux-arm64-gnu": "16.0.6",
"@next/swc-linux-arm64-musl": "16.0.6",
"@next/swc-linux-x64-gnu": "16.0.6",
"@next/swc-linux-x64-musl": "16.0.6",
"@next/swc-win32-arm64-msvc": "16.0.6",
"@next/swc-win32-x64-msvc": "16.0.6",
"@next/swc-darwin-arm64": "16.0.7",
"@next/swc-darwin-x64": "16.0.7",
"@next/swc-linux-arm64-gnu": "16.0.7",
"@next/swc-linux-arm64-musl": "16.0.7",
"@next/swc-linux-x64-gnu": "16.0.7",
"@next/swc-linux-x64-musl": "16.0.7",
"@next/swc-win32-arm64-msvc": "16.0.7",
"@next/swc-win32-x64-msvc": "16.0.7",
"sharp": "^0.34.4"
},
"peerDependencies": {
+1 -1
View File
@@ -12,7 +12,7 @@
"highlight.js": "^11.11.1",
"katex": "^0.16.25",
"lucide-react": "^0.555.0",
"next": "16.0.6",
"next": "^16.0.7",
"react": "19.2.0",
"react-dom": "19.2.0",
"react-markdown": "^10.1.0",
Binary file not shown.

After

Width:  |  Height:  |  Size: 285 KiB

+16 -2
View File
@@ -2,7 +2,7 @@
import { useEffect, useState } from "react";
import { motion } from "framer-motion";
import { Sparkles } from "lucide-react";
import { Paperclip, Sparkles } from "lucide-react";
import WebStuff from "@/components/Web";
import { useParams } from "next/navigation";
import { PostPreview } from "@/lib/posts";
@@ -59,11 +59,25 @@ export default function BlogPost() {
initial="hidden"
animate="visible"
>
<motion.div variants={itemVariants} className="mb-4">
<motion.div
variants={itemVariants}
className="mb-4 w-full flex justify-between items-center"
>
<span className="inline-flex items-center gap-2 px-4 py-2 bg-purple-500/10 border border-purple-500/20 rounded-full text-purple-400 text-sm">
<Sparkles className="w-4 h-4" />
{post.meta.category}
</span>
<span
className="px-3 py-2 bg-slate-500/10 border border-slate-500/20 rounded-full text-slate-400 transition-colors hover:bg-purple-500/10 hover:border-purple-500/20 hover:text-purple-400 text-sm"
onClick={() => {
navigator.clipboard.writeText(
`https://orus-dev.netlify.app${post.meta.url}`
);
}}
>
<Paperclip className="w-4 h-4" />
</span>
</motion.div>
<motion.h1
+15 -25
View File
@@ -39,9 +39,8 @@ export default function Blog() {
: projects.filter((p) => p.category === activeFilter);
const containerVariants = {
hidden: { opacity: 0 },
hidden: {},
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
@@ -49,13 +48,11 @@ export default function Blog() {
};
const itemVariants = {
hidden: { y: 20, opacity: 0 },
hidden: { opacity: 0, y: 20 },
visible: {
y: 0,
opacity: 1,
transition: {
duration: 0.5,
},
y: 0,
transition: { duration: 0.9 },
},
};
@@ -115,22 +112,18 @@ export default function Blog() {
</motion.section>
{/* Blog Posts List */}
<motion.section
className="relative z-10 px-6 py-12 max-w-5xl mx-auto"
initial="hidden"
animate="visible"
variants={containerVariants}
>
<section className="relative z-10 px-6 py-12 max-w-5xl mx-auto">
<div className="flex flex-col gap-8">
{filteredPosts.map((post, i) => (
<motion.article
<motion.div
key={i}
layout
className="relative group bg-slate-900/80 backdrop-blur border border-slate-700 rounded-2xl p-6 hover:border-purple-500/50 transition-all"
whileHover={{ y: -3 }}
className="relative group bg-slate-900/40 backdrop-blur border border-slate-700/50 rounded-2xl p-6 hover:border-purple-500/50 transition-colors"
whileHover={{ y: -3, transition: { duration: 0.5 } }}
variants={itemVariants}
initial="hidden"
animate="visible"
>
<div className="flex flex-col md:flex-row gap-4">
{/* Optional Thumbnail */}
{post.image && (
<div className="shrink-0 w-full md:w-40 h-32 md:h-28 overflow-hidden rounded-xl">
<img
@@ -141,17 +134,15 @@ export default function Blog() {
</div>
)}
{/* Content */}
<div className="flex-1 flex flex-col justify-between">
<div>
<h3 className="text-2xl font-bold mb-2 group-hover:text-purple-400 transition-colors">
{post.title}
<h3 className="text-2xl font-bold mb-2 group-hover:text-purple-400 transition-colors cursor-pointer">
<a href={post.url}>{post.title}</a>
</h3>
<p className="text-slate-300 mb-4 line-clamp-3">
{post.description}
</p>
{/* Metadata */}
<div className="flex flex-wrap items-center gap-3 text-sm text-slate-400 mb-4">
<span>{post.author}</span>
<span>•</span>
@@ -173,7 +164,6 @@ export default function Blog() {
</div>
</div>
{/* Read More */}
<a
href={post.url}
className="self-start mt-auto inline-flex items-center gap-2 px-4 py-2 bg-purple-500/10 hover:bg-purple-500/20 border border-purple-500/20 rounded-lg text-sm font-medium text-purple-400 transition-colors"
@@ -182,10 +172,10 @@ export default function Blog() {
</a>
</div>
</div>
</motion.article>
</motion.div>
))}
</div>
</motion.section>
</section>
</WebStuff>
);
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

+2 -2
View File
@@ -13,8 +13,8 @@ const geistMono = Geist_Mono({
});
export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "ORUS | The community cybersecurity organization",
description: "Hello World! 👋 Welcome to ORUS, a cybersecurity & dev organization,",
};
export default function RootLayout({
+4 -1
View File
@@ -140,8 +140,11 @@ export default function Projects() {
key={i}
layout
className="relative group"
initial={{ opacity: 0, y: 20 }}
whileInView={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, ease: "easeOut" }}
viewport={{ once: true, amount: 0.3 }}
whileHover={{ y: -8 }}
viewport={{ once: false }}
>
<div className="absolute inset-0 bg-linear-to-r from-purple-500/20 to-blue-500/20 rounded-2xl blur-xl group-hover:blur-2xl transition-all" />
<div className="relative bg-slate-900/80 backdrop-blur border border-slate-700 hover:border-purple-500/50 rounded-2xl p-6 transition-all h-full flex flex-col">
+97
View File
@@ -0,0 +1,97 @@
import { AnimatePresence } from "framer-motion";
import { Menu, MessageSquare, X } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { motion } from "framer-motion";
import { createPortal } from "react-dom";
export default function MobileMenu() {
const [open, setOpen] = useState(false);
const [mounted, setMounted] = useState(false);
const buttonRef = useRef<HTMLButtonElement>(null);
const [buttonPosition, setButtonPosition] = useState({ top: 0, right: 0 });
useEffect(() => {
setMounted(true);
}, []);
useEffect(() => {
if (open && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
setButtonPosition({
top: rect.bottom + 8,
right: window.innerWidth - rect.right
});
}
}, [open]);
const menuContent = (
<AnimatePresence>
{open && (
<>
{/* Backdrop */}
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
style={{ zIndex: 999999 }}
className="fixed inset-0 bg-black/20 md:hidden"
onClick={() => setOpen(false)}
/>
{/* Menu */}
<motion.div
initial={{ opacity: 0, y: -10 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -10 }}
transition={{ duration: 0.25 }}
style={{
position: 'fixed',
top: `${buttonPosition.top}px`,
right: `${buttonPosition.right}px`,
zIndex: 1000000
}}
className="w-44 bg-slate-900 border border-slate-700 rounded-xl shadow-2xl p-4 flex flex-col gap-4 md:hidden"
>
{["Blog", "Projects", "About"].map((item) => (
<a
key={item}
href={`/${item.toLowerCase()}`}
className="text-slate-200 hover:text-white transition-colors"
onClick={() => setOpen(false)}
>
{item}
</a>
))}
<a
href="https://discord.gg/HFRsNxfCqT"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-3 py-2 bg-purple-500 hover:bg-purple-600 rounded-lg font-semibold text-sm text-white"
onClick={() => setOpen(false)}
>
<MessageSquare className="w-4 h-4" />
Join Discord
</a>
</motion.div>
</>
)}
</AnimatePresence>
);
return (
<>
<div className="md:hidden">
<button
ref={buttonRef}
onClick={() => setOpen((prev) => !prev)}
className="p-2 text-slate-200"
>
{open ? <X className="w-6 h-6" /> : <Menu className="w-6 h-6" />}
</button>
</div>
{mounted && createPortal(menuContent, document.body)}
</>
);
}
+59
View File
@@ -0,0 +1,59 @@
import { motion } from "framer-motion";
import MobileMenu from "./MobileMenu";
import { MessageSquare } from "lucide-react";
export default function NavMenu() {
return (
<motion.div
className="flex gap-6 items-center"
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: 0.08,
delayChildren: 0.15,
},
},
}}
>
{["Blog", "Projects", "About"].map((item) => (
<motion.a
key={item}
href={`/${item.toLowerCase()}`}
className="text-slate-300 hover:text-white transition-colors hidden md:block"
variants={{
hidden: { opacity: 0, y: -12 },
visible: { opacity: 1, y: 0 },
}}
transition={{
duration: 0.8,
}}
whileHover={{ y: -2 }}
>
{item}
</motion.a>
))}
<motion.a
href="https://discord.gg/HFRsNxfCqT"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 rounded-lg font-semibold transition-colors text-sm"
variants={{
hidden: { opacity: 0, y: -12 },
visible: { opacity: 1, y: 0 },
}}
transition={{
duration: 0.45,
ease: [0.25, 0.1, 0.25, 1],
}}
whileTap={{ scale: 0.95 }}
>
<MessageSquare className="w-4 h-4" />
Join Discord
</motion.a>
<MobileMenu />
</motion.div>
);
}
+5 -52
View File
@@ -1,5 +1,7 @@
import { motion } from "framer-motion";
import { Github, MessageSquare } from "lucide-react";
import MobileMenu from "./MobileMenu";
import NavMenu from "./NavMenu";
export default function WebStuff({
children,
@@ -68,59 +70,10 @@ export default function WebStuff({
animate={{ opacity: 1 }}
transition={{ duration: 0.6, delay: 0.1 }}
>
<img src="orus2.svg" alt="Logo" className="h-4" />
<img src="/orus2.svg" alt="Logo" className="h-4" />
</motion.a>
{/* Menu */}
<motion.div
className="flex gap-6 items-center"
variants={{
hidden: {},
visible: {
transition: {
staggerChildren: 0.08,
delayChildren: 0.15,
},
},
}}
>
{["Blog", "Projects", "About"].map((item) => (
<motion.a
key={item}
href={`/${item.toLowerCase()}`}
className="text-slate-300 hover:text-white transition-colors hidden md:block"
variants={{
hidden: { opacity: 0, y: -12 },
visible: { opacity: 1, y: 0 },
}}
transition={{
duration: 0.8,
}}
whileHover={{ y: -2 }}
>
{item}
</motion.a>
))}
<motion.a
href="https://discord.gg/HFRsNxfCqT"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 bg-purple-500 hover:bg-purple-600 rounded-lg font-semibold transition-colors text-sm"
variants={{
hidden: { opacity: 0, y: -12 },
visible: { opacity: 1, y: 0 },
}}
transition={{
duration: 0.45,
ease: [0.25, 0.1, 0.25, 1],
}}
whileTap={{ scale: 0.95 }}
>
<MessageSquare className="w-4 h-4" />
Join Discord
</motion.a>
</motion.div>
<NavMenu />
</motion.nav>
{children}
@@ -128,7 +81,7 @@ export default function WebStuff({
<footer className="relative z-10 px-6 py-12 border-t border-slate-800/50 mt-20">
<div className="max-w-7xl mx-auto">
<div className="flex flex-col md:flex-row justify-between items-center gap-6">
<img src="orus.svg" alt="Logo" className="h-4" />
<img src="/orus.svg" alt="Logo" className="h-4" />
<div className="text-slate-400 text-center">
© 2025 ORUS. Open source for everyone. Built with 💜 for
+1
View File
@@ -1,4 +1,5 @@
export default function formatNumber(num: number): string {
if (num === null) return num;
if (num < 1000) return num.toString();
if (num < 1_000_000)
return (num / 1000).toFixed(num % 1000 === 0 ? 0 : 1) + "k";