Compare commits
10
Commits
64ae7d0bb7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0812dae8c5 | ||
|
|
57b26f1803 | ||
|
|
73c0c28f8a | ||
|
|
97761b5df4 | ||
|
|
1446b46190 | ||
|
|
5e68b9868c | ||
|
|
12ddf4db4b | ||
|
|
8fb7391d60 | ||
|
|
73ee207912 | ||
|
|
5c9b9ccb17 |
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
Generated
+40
-40
@@ -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
@@ -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 |
@@ -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
@@ -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
@@ -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({
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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
@@ -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,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";
|
||||
|
||||
Reference in New Issue
Block a user