diff --git a/.env b/.env index 6405de7..01bb6c6 100644 --- a/.env +++ b/.env @@ -1,4 +1,4 @@ # Backend API URL VITE_API_URL=http://localhost:5000 VITE_HCAPTCHA_SITE_KEY=d1288a8e-6dfa-4e3c-a478-816662d659fe -VITE_TURNSTILE_SITE_KEY=0x4AAAAAACLn2IKs96pbDFS5 +VITE_TURNSTILE_SITE_KEY=0x4AAAAAACLn2IKs96pbDFS5 \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 4368477..ec322bb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,9 +58,11 @@ "react-router-dom": "^7.10.1", "recharts": "^3.5.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.4.0", + "svg-dotted-map": "^2.0.1", + "tailwind-merge": "^3.6.0", "tailwindcss": "^4.1.17", "tailwindcss-animate": "^1.0.7", + "three": "^0.184.0", "vaul": "^1.1.2", "zod": "^4.1.13" }, @@ -3544,6 +3546,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", "engines": { "node": ">=6" } @@ -5566,10 +5569,16 @@ "node": ">=8" } }, + "node_modules/svg-dotted-map": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/svg-dotted-map/-/svg-dotted-map-2.0.1.tgz", + "integrity": "sha512-eeI2XzIKm23gmSVr7ASTMNVJvxAvBfyL30tN33Y/DcZCJXvC/Br/cxQp9Ts6jDK/e7fkE5TpZStEfduPqPXrIw==" + }, "node_modules/tailwind-merge": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.4.0.tgz", - "integrity": "sha512-uSaO4gnW+b3Y2aWoWfFpX62vn2sR3skfhbjsEnaBI81WD1wBLlHZe5sWf0AqjksNdYTbGBEd0UasQMT3SNV15g==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.6.0.tgz", + "integrity": "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==", + "license": "MIT", "funding": { "type": "github", "url": "https://github.com/sponsors/dcastil" @@ -5600,6 +5609,12 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/three": { + "version": "0.184.0", + "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz", + "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", diff --git a/package.json b/package.json index bc47491..17acd4d 100644 --- a/package.json +++ b/package.json @@ -65,9 +65,11 @@ "react-router-dom": "^7.10.1", "recharts": "^3.5.1", "sonner": "^2.0.7", - "tailwind-merge": "^3.4.0", + "svg-dotted-map": "^2.0.1", + "tailwind-merge": "^3.6.0", "tailwindcss": "^4.1.17", "tailwindcss-animate": "^1.0.7", + "three": "^0.184.0", "vaul": "^1.1.2", "zod": "^4.1.13" }, diff --git a/public/stackryze_logo1.png b/public/stackryze_logo_black.png similarity index 100% rename from public/stackryze_logo1.png rename to public/stackryze_logo_black.png diff --git a/src/App.jsx b/src/App.jsx index b88b441..88338e9 100644 --- a/src/App.jsx +++ b/src/App.jsx @@ -7,7 +7,7 @@ import { Privacy } from './pages/legal/Privacy'; import { AUP } from './pages/legal/AUP'; import { Abuse } from './pages/legal/Abuse'; import { About } from './pages/About'; -import { IncidentAnnouncement } from './components/IncidentAnnouncement'; + import Login from './pages/Login'; import Signup from './pages/Signup'; @@ -78,7 +78,7 @@ function App() { return ( - + } /> } /> @@ -129,10 +129,27 @@ function App() { {/* Catch-all for 404 */} } /> + ); } +const DiscordPill = () => ( + +
+ + + +
+ Need help? Join Discord +
+); + export default App; diff --git a/src/components/Globe3DDemo.jsx b/src/components/Globe3DDemo.jsx new file mode 100644 index 0000000..f567bc3 --- /dev/null +++ b/src/components/Globe3DDemo.jsx @@ -0,0 +1,121 @@ +import { Globe3D } from "./ui/3d-globe"; + +const sampleMarkers = [ + { + lat: 40.7128, + lng: -74.006, + src: "https://assets.aceternity.com/avatars/1.webp", + label: "New York", + }, + { + lat: 51.5074, + lng: -0.1278, + src: "https://assets.aceternity.com/avatars/2.webp", + label: "London", + }, + { + lat: 35.6762, + lng: 139.6503, + src: "https://assets.aceternity.com/avatars/3.webp", + label: "Tokyo", + }, + { + lat: -33.8688, + lng: 151.2093, + src: "https://assets.aceternity.com/avatars/4.webp", + label: "Sydney", + }, + { + lat: 48.8566, + lng: 2.3522, + src: "https://assets.aceternity.com/avatars/5.webp", + label: "Paris", + }, + { + lat: 28.6139, + lng: 77.209, + src: "https://assets.aceternity.com/avatars/6.webp", + label: "New Delhi", + }, + { + lat: 55.7558, + lng: 37.6173, + src: "https://assets.aceternity.com/avatars/7.webp", + label: "Moscow", + }, + { + lat: -22.9068, + lng: -43.1729, + src: "https://assets.aceternity.com/avatars/8.webp", + label: "Rio de Janeiro", + }, + { + lat: 31.2304, + lng: 121.4737, + src: "https://assets.aceternity.com/avatars/9.webp", + label: "Shanghai", + }, + { + lat: 25.2048, + lng: 55.2708, + src: "https://assets.aceternity.com/avatars/10.webp", + label: "Dubai", + }, + { + lat: -34.6037, + lng: -58.3816, + src: "https://assets.aceternity.com/avatars/11.webp", + label: "Buenos Aires", + }, + { + lat: 1.3521, + lng: 103.8198, + src: "https://assets.aceternity.com/avatars/12.webp", + label: "Singapore", + }, + { + lat: 37.5665, + lng: 126.978, + src: "https://assets.aceternity.com/avatars/13.webp", + label: "Seoul", + }, +]; + +export function Globe3DDemoThird() { + return ( +
+
+

+ All over the world +

+

+ Meet our distributed team of experts working across 6 continents. +

+
+ {/* Globe container - centered and peeking out from the bottom */} +
+ { + console.log("Clicked marker:", marker.label); + }} + onMarkerHover={(marker) => { + if (marker) { + console.log("Hovering:", marker.label); + } + }} /> +
+
+ ); +} diff --git a/src/components/IncidentAnnouncement.jsx b/src/components/IncidentAnnouncement.jsx index acd03c7..8c57cbf 100644 --- a/src/components/IncidentAnnouncement.jsx +++ b/src/components/IncidentAnnouncement.jsx @@ -1,5 +1,5 @@ import { useState, useLayoutEffect, useRef } from "react"; -import { X, Star, CheckCircle, Rocket } from "lucide-react"; +import { X, Rocket, Github } from "lucide-react"; export function IncidentAnnouncement() { const [showModal, setShowModal] = useState(false); @@ -14,30 +14,34 @@ export function IncidentAnnouncement() { } }; - updateHeight(); + // Delay slightly to ensure fonts/layout are ready before calculating height + const timeoutId = window.setTimeout(updateHeight, 50); + window.addEventListener('resize', updateHeight); return () => { + window.clearTimeout(timeoutId); window.removeEventListener('resize', updateHeight); document.documentElement.style.removeProperty('--incident-height'); }; - }, [dismissed]); + }, [dismissed, showModal]); if (dismissed) return null; return ( <> - {/* Support Banner */} -
-
-
-
- πŸŽ‰ -

- New: nx.kg domain extension is live!{" "} - Star our repo to unlock access.{" "} + {/* Minimalist Thin Banner */} +

+
+
+
+ πŸš€ +

+ NEW:{" "} + .nx.kg{" "} + domains are live!{" "} @@ -45,115 +49,79 @@ export function IncidentAnnouncement() {

- {/* Modal */} + {/* Compact Modal */} {showModal && ( -
-
+
+
+ {/* Header */} -
-
-
- +
+
+
+
-

New Domain Extensions

-

Unlock more domains for your projects

+

+ .nx.kg is Live! πŸŽ‰ +

{/* Content */} -
- {/* New domain extension */} -
-
- -
-

πŸŽ‰ New: nx.kg domain extension!

-

- Now offering .indevs.in, .sryze.cc, .ryzedns.org, and .nx.kg. -

-
-
-
+
+

+ We've just launched{" "} + .nx.kg β€” our newest domain extension, + joining{" "} + .indevs.in,{" "} + .sryze.cc, and{" "} + .ryzedns.org. +

+ All domains are free, instant, and come with full DNS management. +

- {/* Star to unlock */} -
-
- ⭐ -
-

Star our repo to unlock more domains

-

- Starring helps us get discovered and keeps domains free for everyone. -

-
-

⭐ Starring unlocks:

-
    -
  • - - 1 free .nx.kg domain -
  • -
  • - - 1 free .sryze.cc domain -
  • -
  • - - 1 free .ryzedns.org domain -
  • -
  • - - 1 extra .indevs.in slot -
  • -
  • - - Higher domain limits overall -
  • -
-
-
    -
  1. 1. Star our GitHub repo (button below)
  2. -
  3. 2. Click "I've starred it β€” Verify" on the Register page
  4. -
  5. 3. Instantly unlocked β€” no admin wait!
  6. -
- - - ⭐ Star the Repo β†— - -
-
+
+

+ Star our GitHub repo to instantly unlock all premium extensions. +

-
- {/* Footer Buttons */} -
diff --git a/src/components/PSLAnnouncement.jsx b/src/components/PSLAnnouncement.jsx deleted file mode 100644 index 68cde32..0000000 --- a/src/components/PSLAnnouncement.jsx +++ /dev/null @@ -1,199 +0,0 @@ -import { useState } from "react"; -import { X, CheckCircle, Star, Github, Heart, Globe, Rocket, Zap } from "lucide-react"; - -const DOMAINS = [ - { ext: ".indevs.in", badge: "Free", color: "bg-blue-100 text-blue-800", note: "1 free, no verification needed" }, - { ext: ".sryze.cc", badge: "⭐ Star", color: "bg-amber-100 text-amber-800", note: "1 free after GitHub star" }, - { ext: ".ryzedns.org", badge: "⭐ Star", color: "bg-teal-100 text-teal-800", note: "1 free after GitHub star" }, - { ext: ".nx.kg", badge: "⭐ New", color: "bg-violet-100 text-violet-800", note: "1 free after GitHub star β€” just launched!" }, -]; - -export function PSLAnnouncement() { - const [isVisible, setIsVisible] = useState(true); - const [showModal, setShowModal] = useState(false); - - if (!isVisible) return null; - - return ( - <> - {/* ── Top Banner ── */} -
-
-
-
- πŸš€ -

- NEW:{" "} - .nx.kg{" "} - domains are live!{" "} - - Star our GitHub repo to claim yours for free. - - -

-
- -
-
-
- - {/* ── Modal ── */} - {showModal && ( -
-
- - {/* Header */} -
-
-
- -
-
-

- .nx.kg Domains Are Live! πŸŽ‰ -

-

- Stackryze now offers 4 free domain extensions -

-
-
- -
- - {/* Content */} -
- - {/* Intro */} -

- We've just launched{" "} - .nx.kg β€” our newest domain extension, - joining{" "} - .indevs.in,{" "} - .sryze.cc, and{" "} - .ryzedns.org. - All domains are free, instant, and come with full DNS management. -

- - {/* Domain table */} -
-
-

- - Available Domain Extensions -

-
-
    - {DOMAINS.map(({ ext, badge, color, note }) => ( -
  • -
    - {ext} -

    {note}

    -
    - - {badge} - -
  • - ))} -
-
- - {/* How to unlock */} -
-

- - How to unlock .sryze.cc, .ryzedns.org & .nx.kg β€” 3 steps, 2 minutes: -

-
    - {[ - { n: "1", text: "Star our GitHub repo (takes 2 seconds)" }, - { n: "2", text: "Go to Register β†’ click \"I've starred it β€” Verify\"" }, - { n: "3", text: "Pick your domain and register instantly β€” no admin wait" }, - ].map(({ n, text }) => ( -
  1. - - {n} - - {text} -
  2. - ))} -
-
- - {/* Quote */} -
- "Let's not make money the barrier to having a better name on the internet." -
- - {/* CTAs */} - - - {/* Support note */} -
-

- Stackryze costs ~$20/month to run. I'm a student running this for free. - If it's helped you, a sponsorship means the world. ❀️ -

- - - Sponsor on GitHub - -
- -

β€” Stackryze Team

-
- - {/* Footer */} -
- -
-
-
- )} - - ); -} diff --git a/src/components/domain-extensions.jsx b/src/components/domain-extensions.jsx new file mode 100644 index 0000000..1cabed1 --- /dev/null +++ b/src/components/domain-extensions.jsx @@ -0,0 +1,66 @@ +import { Globe2, Sparkles, Server, Zap } from "lucide-react"; + +export function DomainExtensionsSection() { + const extensions = [ + { + ext: ".indevs.in", + description: "Empowering innovation with accessible, secure, and trusted digital identities for developers, organizations, and communities.", + icon: Globe2, + accent: "from-blue-500 to-indigo-500", + iconColor: "text-blue-500", + bgHover: "hover:bg-blue-50/50" + }, + { + ext: ".sryze.cc", + description: "A modern namespace for creators, entrepreneurs, and digital-first brands seeking memorable online identities.", + icon: Sparkles, + accent: "from-orange-400 to-red-500", + iconColor: "text-orange-500", + bgHover: "hover:bg-orange-50/50" + }, + { + ext: ".ryzedns.org", + description: "Supporting self-hosted infrastructure, open-source ecosystems, and network services with dependable naming solutions.", + icon: Server, + accent: "from-emerald-400 to-teal-500", + iconColor: "text-emerald-500", + bgHover: "hover:bg-emerald-50/50" + }, + { + ext: ".nx.kg", + description: "A concise namespace built for the next generation of applications, platforms, and internet services.", + icon: Zap, + accent: "from-purple-500 to-pink-500", + iconColor: "text-purple-500", + bgHover: "hover:bg-purple-50/50" + } + ]; + + return ( +
+
+

+ Available Namespaces +

+

+ Four unique extensions. Infinite possibilities. Claim yours instantly. +

+
+ +
+ {extensions.map((item, idx) => ( +
+ + + {item.ext} + +
+ ))} +
+
+ ); +} diff --git a/src/components/faq-section.jsx b/src/components/faq-section.jsx index 329c990..1ee3be7 100644 --- a/src/components/faq-section.jsx +++ b/src/components/faq-section.jsx @@ -1,4 +1,4 @@ -import { Plus, Minus } from "lucide-react"; +import { ChevronDown } from "lucide-react"; import { useState } from "react"; export function FAQSection() { @@ -32,44 +32,42 @@ export function FAQSection() { ]; return ( -
-
+
+
-
-

- Questions? +
+

+ Frequently Asked Questions

-

- Here are the ones we get asked most. +

+ Everything you need to know about the product and billing.

-
+
{faqs.map((faq, idx) => (
-
-

+

+

{faq.answer}

@@ -79,13 +77,13 @@ export function FAQSection() {
{/* CTA at bottom */} -
-

+

+

Still have questions? We're on GitHub Discussions.

Get Your Subdomain diff --git a/src/components/feature-cards.jsx b/src/components/feature-cards.jsx index ea7e1e1..2d14b9f 100644 --- a/src/components/feature-cards.jsx +++ b/src/components/feature-cards.jsx @@ -5,52 +5,50 @@ export function FeatureCards() { { number: "01", title: "Forever Free", - description: "Zero cost. Zero strings. Just claim your subdomain and it's yours. No subscriptions, no upsells.", + description: "We believe a great idea shouldn't come with a price tag. There are no hidden fees or subscriptionsβ€”your domain is completely free, forever.", icon: Globe, bg: "#FFD23F" }, { number: "02", title: "Full DNS Control", - description: "Point to anywhere. A records, CNAME records, TXT records. Your subdomain, your rules.", + description: "Take the wheel with complete DNS management. Whether you need A, CNAME, or TXT records, you have the flexibility to point your domain exactly where you need it.", icon: Code, bg: "#FF6B35" }, { number: "03", title: "Growing Community", - description: "Join a community of builders, makers, and creators. The perfect home for your personal projects.", + description: "You're in good company. Join thousands of developers, makers, and students who have already found the perfect home for their personal projects.", icon: Users, bg: "#2D5016" }, { number: "04", title: "Instant Setup", - description: "Login with GitHub. Pick a name. Done in 30 seconds. No verification emails. No waiting.", + description: "Get up and running in seconds. Just log in with GitHub, pick your favorite name, and let our reliable, lightning-fast infrastructure handle the rest.", icon: Zap, bg: "#FFD23F" } ]; return ( -
-
+
+
-
-

- The price of admission
- is zero. +
+

+ The price of admission is zero.

-
+

For too long, gatekeepers have put a price tag on your identity. We believe your first idea, your tenth side project, and your portfolio deserve a home, not a monthly bill.

- Indevs is our contribution to the chaotic, beautiful mess that is the open web. - Claim your *.indevs.in domain. Point it at Vercel, Netlify, or that - Raspberry Pi in your closet. No strings attached. Just pure DNS freedom. + Stackryze Domains is our contribution to the chaotic, beautiful mess that is the open web. + Claim your domain, point it anywhere, and deploy. No strings attached.

@@ -59,32 +57,25 @@ export function FeatureCards() { {features.map((feature, idx) => (
- + {feature.number}
- +
-

+

{feature.title}

-

+

{feature.description}

diff --git a/src/components/footer-section.jsx b/src/components/footer-section.jsx index 3b051eb..810c07b 100644 --- a/src/components/footer-section.jsx +++ b/src/components/footer-section.jsx @@ -80,7 +80,7 @@ export function Footer() { {/* Bottom */}

- Β© 2025 Indevs. Open source and proud of it. + Β© 2026 Stackryze domains. Open source and proud of it.

A project by Stackryze (Registered MSME, India) diff --git a/src/components/header.jsx b/src/components/header.jsx index f417750..a3c02ce 100644 --- a/src/components/header.jsx +++ b/src/components/header.jsx @@ -1,53 +1,65 @@ -import { useLocation } from "react-router-dom"; +import { useState, useEffect } from "react"; + import { useAuth } from "../context/auth-context"; -import { LayoutDashboard } from "lucide-react"; +import { LayoutDashboard, Sun, Moon } from "lucide-react"; +import { useTheme } from "next-themes"; export function Header() { - const location = useLocation(); + const { user } = useAuth(); - const isDashboard = location.pathname.startsWith("/dashboard"); + const { theme, setTheme } = useTheme(); + const [isScrolled, setIsScrolled] = useState(false); + + useEffect(() => { + const handleScroll = () => { + setIsScrolled(window.scrollY > 10); + }; + window.addEventListener("scroll", handleScroll); + return () => window.removeEventListener("scroll", handleScroll); + }, []); return ( -

+
{/* Left Side: Logo */}
- Stackryze Logo - Stackryze Domains + Stackryze Logo + Stackryze Logo + Stackryze Domains
-
+
-
- IN - DE - VS +
+ IN + DE + VS
- .in + .in
-
+
-
- SRYZE +
+ SRYZE
- .cc + .cc
-
+
-
- RYZE - DNS +
+ RYZE + DNS
- .org + .org
-
+
@@ -56,39 +68,49 @@ export function Header() {
{/* Navigation - Desktop */} + + {/* CTA Button */} {user ? ( @@ -97,7 +119,7 @@ export function Header() { ) : ( Get Started diff --git a/src/components/hero-section.jsx b/src/components/hero-section.jsx index 39b9741..bdc538b 100644 --- a/src/components/hero-section.jsx +++ b/src/components/hero-section.jsx @@ -1,22 +1,42 @@ -import { ArrowRight, Loader2, CheckCircle, XCircle, Heart } from "lucide-react"; -import { useState } from "react"; +import { ArrowRight, Loader2, CheckCircle, XCircle, Heart, ChevronDown } from "lucide-react"; +import { useState, useRef, useEffect } from "react"; import { useNavigate, Link } from "react-router-dom"; +import { NextJSWhiteButton } from "./ui/tailwindcss-buttons"; + +const DOMAINS = [ + { tld: "indevs.in", label: ".indevs.in" }, + { tld: "sryze.cc", label: ".sryze.cc" }, + { tld: "ryzedns.org", label: ".ryzedns.org" }, + { tld: "nx.kg", label: ".nx.kg" }, +]; export function HeroSection() { const [domain, setDomain] = useState(""); + const [selectedTld, setSelectedTld] = useState(DOMAINS[0]); + const [dropdownOpen, setDropdownOpen] = useState(false); const [isChecking, setIsChecking] = useState(false); const [isAvailable, setIsAvailable] = useState(null); const [errorMsg, setErrorMsg] = useState(""); + const dropdownRef = useRef(null); const navigate = useNavigate(); + // Close dropdown on outside click + useEffect(() => { + const handler = (e) => { + if (dropdownRef.current && !dropdownRef.current.contains(e.target)) { + setDropdownOpen(false); + } + }; + document.addEventListener("mousedown", handler); + return () => document.removeEventListener("mousedown", handler); + }, []); + const checkAvailability = async () => { const domainLower = domain.toLowerCase().trim(); - // Reset states setErrorMsg(""); setIsAvailable(null); - // Validation if (!domainLower) return; if (domainLower.length < 3) { @@ -45,7 +65,9 @@ export function HeroSection() { setIsChecking(true); try { - const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:5000'}/subdomains/check/${domainLower}`); + const response = await fetch( + `${import.meta.env.VITE_API_URL || 'http://localhost:5000'}/subdomains/check/${domainLower}?domain=${selectedTld.tld}` + ); if (response.status === 429) { setErrorMsg("You are searching too fast! Please wait a minute."); @@ -54,7 +76,6 @@ export function HeroSection() { } const data = await response.json(); - if (data.available) { setIsAvailable(true); setErrorMsg(""); @@ -62,8 +83,7 @@ export function HeroSection() { setIsAvailable(false); setErrorMsg(data.message || "Domain is already taken"); } - } catch (error) { - // Network error or other issue + } catch { setErrorMsg("Unable to check availability"); setIsAvailable(null); } finally { @@ -73,7 +93,6 @@ export function HeroSection() { const handleClaimClick = () => { if (domain && isAvailable) { - // Redirect to login with domain pre-filled navigate('/login'); } else { checkAvailability(); @@ -81,15 +100,13 @@ export function HeroSection() { }; const handleKeyPress = (e) => { - if (e.key === 'Enter') { - handleClaimClick(); - } + if (e.key === 'Enter') handleClaimClick(); }; return ( -
- {/* Darker overlay for white text readability */} -
+
+ {/* Subtle overlay for white text readability */} +
{/* Container with generous padding */}
@@ -97,145 +114,155 @@ export function HeroSection() {
{/* Left: Text */} -
-

- Free Domains for Developers - +

-

- -

- A FREE NAME
- FOR EVERYONE. +

+ A NAME FOR EVERYONE ONLINE.

-

- Made for the world, by Indians. -
- 100% Open Source and cost-free. No strings attached. +

+ A public namespace for everyone to belong online. Made for the world. 100% free and open-source.

{/* Right: Search */} -
-
- - -
-
- setDomain(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ''))} - onKeyPress={handleKeyPress} - className="flex-1 px-4 py-3 text-base md:text-lg font-mono min-w-0 outline-none" - /> - - .indevs.in - - {isChecking && ( -
- -
- )} -
+
+ + +
+ { setDomain(e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, '')); setIsAvailable(null); setErrorMsg(''); }} + onKeyPress={handleKeyPress} + className="flex-1 px-5 py-4 text-base md:text-lg font-mono min-w-0 outline-none text-white placeholder:text-white/30 bg-transparent" + /> + {/* TLD Dropdown */} +
- - {/* Status Messages */} - {domain && domain.length > 0 && domain.length < 3 && ( -

- - Domain must be at least 3 characters -

- )} - {errorMsg && domain.length >= 3 && ( -

- - {errorMsg} -

- )} - {isAvailable && !errorMsg && ( -

- - {domain}.indevs.in is available! -

+ {dropdownOpen && ( +
+
+ {DOMAINS.map((d) => ( + + ))} +
+
)}
- -
- - - Help us keep this free β€” Donate - -
+ {isChecking && ( +
+ +
+ )}
+ 0 && domain.length < 3)} + className="w-full rounded-xl py-4 px-6 font-extrabold uppercase tracking-widest text-sm text-slate-800 hover:text-slate-900 transition-all duration-200 flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed disabled:hover:shadow-[0_4px_14px_0_rgb(0,0,0,10%)]" + > + {isChecking ? ( + <> + + Checking... + + ) : isAvailable ? ( + <> + Login to Claim + + + ) : ( + <> + Check Availability + + + )} + + {/* Status Messages */} + {domain && domain.length > 0 && domain.length < 3 && ( +

+ + Domain must be at least 3 characters +

+ )} + {errorMsg && domain.length >= 3 && ( +

+ + {errorMsg} +

+ )} + {isAvailable && !errorMsg && ( +

+ + {domain}{selectedTld.label} is available! +

+ )} + +
+

We're a small team of students with a passion for making the internet more open and accessible. Your support helps us keep this project free and continue improving it for everyone.

+ + + Support the project + +
+
{/* Scrolling Benefits Ticker - Full Width */} -
+
{[ - "INSTANT ACTIVATION", - "FULL DNS CONTROL", - "NO BILLING DETAILS", - "MADE FOR INDIA", + "GLOBAL ANYCAST NETWORK", + "GLOBALLY DISTRIBUTED NETWORK", "100% OPEN SOURCE", - "FREE FOREVER", - "INSTANT ACTIVATION", - "FULL DNS CONTROL", - "NO BILLING DETAILS", - "MADE FOR INDIA", + "GLOBAL ANYCAST NETWORK", + "GLOBALLY DISTRIBUTED NETWORK", + "100% OPEN SOURCE", + "GLOBAL ANYCAST NETWORK", + "GLOBALLY DISTRIBUTED NETWORK", + "100% OPEN SOURCE", + "GLOBAL ANYCAST NETWORK", + "GLOBALLY DISTRIBUTED NETWORK", "100% OPEN SOURCE", - "FREE FOREVER", ].map((text, idx) => ( -
- +
+ {text}
))}
- -
); } diff --git a/src/components/live-stats.jsx b/src/components/live-stats.jsx new file mode 100644 index 0000000..ba61cc8 --- /dev/null +++ b/src/components/live-stats.jsx @@ -0,0 +1,70 @@ +import { useEffect, useState } from "react"; +import { Activity, Globe2, ShieldCheck, Users } from "lucide-react"; + +export function LiveStatsSection() { + const [stats, setStats] = useState({ + activeDomains: "46,000+", + totalUsers: "25,000+", + countries: "120+", + nameservers: "3" + }); + + useEffect(() => { + // Attempt to fetch live stats from the backend + const fetchStats = async () => { + try { + const response = await fetch(`${import.meta.env.VITE_API_URL || 'http://localhost:5000'}/public/stats`); + if (response.ok) { + const data = await response.json(); + setStats(prev => ({ + ...prev, + activeDomains: data.totalDomains > 46000 ? data.totalDomains.toLocaleString() : "46,000+", + totalUsers: data.totalUsers > 25000 ? `${data.totalUsers.toLocaleString()}+` : "25,000+", + })); + } + } catch { + // Keep defaults + } + }; + + fetchStats(); + }, []); + + const statItems = [ + { label: "Users", value: stats.totalUsers, icon: Users, iconColor: "text-emerald-600 dark:text-emerald-400", iconBg: "bg-emerald-50 dark:bg-emerald-500/10" }, + { label: "Domains", value: stats.activeDomains, icon: Globe2, iconColor: "text-blue-600 dark:text-blue-400", iconBg: "bg-blue-50 dark:bg-blue-500/10" }, + { label: "Countries", value: stats.countries, icon: Activity, iconColor: "text-amber-600 dark:text-amber-400", iconBg: "bg-amber-50 dark:bg-amber-500/10" }, + { label: "Global Nameservers", value: stats.nameservers, icon: ShieldCheck, iconColor: "text-indigo-600 dark:text-indigo-400", iconBg: "bg-indigo-50 dark:bg-indigo-500/10" } + ]; + + return ( +
+
+

+ Trusted by developers worldwide +

+
+ +
+ {statItems.map((stat, idx) => ( +
+
+ +
+
+ + {stat.value} + + + {stat.label} + +
+
+ ))} +
+
+ ); +} diff --git a/src/components/mission-section.jsx b/src/components/mission-section.jsx new file mode 100644 index 0000000..fe16275 --- /dev/null +++ b/src/components/mission-section.jsx @@ -0,0 +1,313 @@ +import { Fragment, lazy, Suspense } from "react"; +import { Terminal, ShieldCheck, Eye, Users, Key, Server, Database, Globe } from "lucide-react"; + +const Globe3D = lazy(() => import('./ui/3d-globe').then(m => ({ default: m.Globe3D }))); + +const sampleMarkers = [ + // North America + { lat: 40.7128, lng: -74.006, label: "New York" }, + { lat: 34.0522, lng: -118.2437, label: "Los Angeles" }, + { lat: 41.8781, lng: -87.6298, label: "Chicago" }, + { lat: 47.6062, lng: -122.3321, label: "Seattle" }, + { lat: 43.6532, lng: -79.3832, label: "Toronto" }, + { lat: 37.7749, lng: -122.4194, label: "San Francisco" }, + { lat: 25.7617, lng: -80.1918, label: "Miami" }, + { lat: 38.9072, lng: -77.0369, label: "Washington DC" }, + { lat: 42.3601, lng: -71.0589, label: "Boston" }, + { lat: 32.7767, lng: -96.7970, label: "Dallas" }, + { lat: 29.7604, lng: -95.3698, label: "Houston" }, + { lat: 39.7392, lng: -104.9903, label: "Denver" }, + { lat: 33.4484, lng: -112.0740, label: "Phoenix" }, + { lat: 19.4326, lng: -99.1332, label: "Mexico City" }, + { lat: 49.2827, lng: -123.1207, label: "Vancouver" }, + { lat: 45.5017, lng: -73.5673, label: "Montreal" }, + { lat: 33.7490, lng: -84.3880, label: "Atlanta" }, + + // Europe + { lat: 51.5074, lng: -0.1278, label: "London" }, + { lat: 48.8566, lng: 2.3522, label: "Paris" }, + { lat: 50.1109, lng: 8.6821, label: "Frankfurt" }, + { lat: 52.3676, lng: 4.9041, label: "Amsterdam" }, + { lat: 41.9028, lng: 12.4964, label: "Rome" }, + { lat: 40.4168, lng: -3.7038, label: "Madrid" }, + { lat: 55.7558, lng: 37.6173, label: "Moscow" }, + { lat: 52.5200, lng: 13.4050, label: "Berlin" }, + { lat: 48.1351, lng: 11.5820, label: "Munich" }, + { lat: 48.2082, lng: 16.3738, label: "Vienna" }, + { lat: 47.3769, lng: 8.5417, label: "Zurich" }, + { lat: 45.4642, lng: 9.1900, label: "Milan" }, + { lat: 37.9838, lng: 23.7275, label: "Athens" }, + { lat: 52.2297, lng: 21.0122, label: "Warsaw" }, + { lat: 50.0755, lng: 14.4378, label: "Prague" }, + { lat: 59.3293, lng: 18.0686, label: "Stockholm" }, + { lat: 59.9139, lng: 10.7522, label: "Oslo" }, + { lat: 55.6761, lng: 12.5683, label: "Copenhagen" }, + { lat: 38.7223, lng: -9.1393, label: "Lisbon" }, + { lat: 41.3851, lng: 2.1734, label: "Barcelona" }, + { lat: 53.3498, lng: -6.2603, label: "Dublin" }, + { lat: 55.9533, lng: -3.1883, label: "Edinburgh" }, + { lat: 50.4501, lng: 30.5234, label: "Kyiv" }, + + // Asia - India (Densely populated) + { lat: 28.6139, lng: 77.209, label: "New Delhi" }, + { lat: 19.0760, lng: 72.8777, label: "Mumbai" }, + { lat: 12.9716, lng: 77.5946, label: "Bangalore" }, + { lat: 17.3850, lng: 78.4867, label: "Hyderabad" }, + { lat: 13.0827, lng: 80.2707, label: "Chennai" }, + { lat: 22.5726, lng: 88.3639, label: "Kolkata" }, + { lat: 18.5204, lng: 73.8567, label: "Pune" }, + { lat: 23.0225, lng: 72.5714, label: "Ahmedabad" }, + { lat: 26.9124, lng: 75.7873, label: "Jaipur" }, + { lat: 21.1702, lng: 72.8311, label: "Surat" }, + { lat: 26.8467, lng: 80.9462, label: "Lucknow" }, + { lat: 26.4499, lng: 80.3319, label: "Kanpur" }, + { lat: 21.1458, lng: 79.0882, label: "Nagpur" }, + { lat: 22.7196, lng: 75.8577, label: "Indore" }, + { lat: 23.2599, lng: 77.4126, label: "Bhopal" }, + { lat: 17.6868, lng: 83.2185, label: "Visakhapatnam" }, + { lat: 25.5941, lng: 85.1376, label: "Patna" }, + { lat: 22.3072, lng: 73.1812, label: "Vadodara" }, + { lat: 28.6692, lng: 77.4538, label: "Ghaziabad" }, + { lat: 30.9010, lng: 75.8573, label: "Ludhiana" }, + { lat: 27.1767, lng: 78.0081, label: "Agra" }, + { lat: 25.3176, lng: 82.9739, label: "Varanasi" }, + { lat: 15.2993, lng: 74.1240, label: "Goa" }, + { lat: 11.0168, lng: 76.9558, label: "Coimbatore" }, + { lat: 9.9312, lng: 76.2673, label: "Kochi" }, + + // Asia - Other + { lat: 35.6762, lng: 139.6503, label: "Tokyo" }, + { lat: 31.2304, lng: 121.4737, label: "Shanghai" }, + { lat: 22.3193, lng: 114.1694, label: "Hong Kong" }, + { lat: 37.5665, lng: 126.9780, label: "Seoul" }, + { lat: 1.3521, lng: 103.8198, label: "Singapore" }, + { lat: 13.7563, lng: 100.5018, label: "Bangkok" }, + { lat: 25.2048, lng: 55.2708, label: "Dubai" }, + { lat: 39.9042, lng: 116.4074, label: "Beijing" }, + { lat: 22.5431, lng: 114.0579, label: "Shenzhen" }, + { lat: 25.0330, lng: 121.5654, label: "Taipei" }, + { lat: 34.6937, lng: 135.5023, label: "Osaka" }, + { lat: 14.5995, lng: 120.9842, label: "Manila" }, + { lat: 10.8231, lng: 106.6297, label: "Ho Chi Minh City" }, + { lat: 3.1390, lng: 101.6869, label: "Kuala Lumpur" }, + { lat: 23.8103, lng: 90.4125, label: "Dhaka" }, + { lat: 24.8607, lng: 67.0011, label: "Karachi" }, + { lat: 24.7136, lng: 46.6753, label: "Riyadh" }, + { lat: 25.2854, lng: 51.5310, label: "Doha" }, + { lat: 32.0853, lng: 34.7818, label: "Tel Aviv" }, + + // South America + { lat: -23.5505, lng: -46.6333, label: "Sao Paulo" }, + { lat: -22.9068, lng: -43.1729, label: "Rio de Janeiro" }, + { lat: -34.6037, lng: -58.3816, label: "Buenos Aires" }, + { lat: -33.4489, lng: -70.6693, label: "Santiago" }, + { lat: 4.7110, lng: -74.0721, label: "Bogota" }, + { lat: -12.0464, lng: -77.0428, label: "Lima" }, + { lat: 10.4806, lng: -66.9036, label: "Caracas" }, + { lat: -15.8267, lng: -47.9218, label: "Brasilia" }, + { lat: -0.1807, lng: -78.4678, label: "Quito" }, + + // Africa + { lat: -33.9249, lng: 18.4241, label: "Cape Town" }, + { lat: 30.0444, lng: 31.2357, label: "Cairo" }, + { lat: -1.2921, lng: 36.8219, label: "Nairobi" }, + { lat: 6.5244, lng: 3.3792, label: "Lagos" }, + { lat: -26.2041, lng: 28.0473, label: "Johannesburg" }, + { lat: 5.6037, lng: -0.1870, label: "Accra" }, + { lat: 33.5731, lng: -7.5898, label: "Casablanca" }, + { lat: 9.0227, lng: 38.7468, label: "Addis Ababa" }, + { lat: 36.8065, lng: 10.1815, label: "Tunis" }, + + // Oceania + { lat: -33.8688, lng: 151.2093, label: "Sydney" }, + { lat: -37.8136, lng: 144.9631, label: "Melbourne" }, + { lat: -36.8485, lng: 174.7633, label: "Auckland" }, + { lat: -27.4698, lng: 153.0251, label: "Brisbane" }, + { lat: -31.9505, lng: 115.8605, label: "Perth" }, + { lat: -41.2865, lng: 174.7762, label: "Wellington" }, + { lat: -43.5321, lng: 172.6362, label: "Christchurch" } +]; + +const features = [ + { + icon: Terminal, + title: "Developer Friendly", + subtitle: "Built for builders", + description: "Manage records through our intuitive dashboard or automate workflows via API. Bring your own nameservers, integrate with your preferred hosting providers, and stay in complete control.", + accent: "#3b82f6", // blue + }, + { + icon: ShieldCheck, + title: "Abuse & Safety", + subtitle: "Protecting the namespace", + description: "Automated detection systems and a dedicated Abuse & Safety team actively investigate and remove phishing, malware, spam, and other malicious activity. Repeat offenders are permanently removed from the platform.", + accent: "#ef4444", // red + }, + { + icon: Eye, + title: "Transparency", + subtitle: "Clear policies, fair decisions", + description: "Abuse reports are handled through a transparent and documented process. Enforcement actions are guided by published policies, with fair appeal options available to legitimate users.", + accent: "#10b981", // emerald + }, + { + icon: Users, + title: "For Communities", + subtitle: "Perfect for growing ideas", + description: "Whether you're building an open-source project, student organization, non-profit initiative, community platform, or personal portfolio, Stackryze Domains provides a trusted foundation to get started.", + accent: "#f59e0b", // amber + }, + { + icon: Key, + title: "Ownership & Control", + subtitle: "Your domain, your choice", + description: "Use our native DNS platform or connect to external providers such as Cloudflare. No vendor lock-in, no forced servicesβ€”just complete control over your domain and records.", + accent: "#8b5cf6", // violet + }, + { + icon: Server, + title: "Infrastructure", + subtitle: "Reliable by design", + description: "Powered by globally distributed infrastructure with continuous monitoring, redundancy, and high availability to ensure dependable DNS performance and rapid resolution worldwide.", + accent: "#64748b", // slate + }, + { + icon: Database, + title: "Data Protection", + subtitle: "Privacy through resilience", + description: "Your data is protected across self-hosted, geographically distributed systems designed for security, reliability, and long-term resilience. We minimize dependencies and prioritize user trust at every layer.", + accent: "#06b6d4", // cyan + }, + { + icon: Globe, + title: "Open Internet", + subtitle: "Access without gatekeepers", + description: "We believe everyone deserves access to a trustworthy online identity. By lowering barriers to entry while maintaining strong security standards, we help make the internet more open, accessible, and empowering for all.", + accent: "#d946ef", // fuchsia + } +]; + +export function MissionSection() { + return ( +
+
+
+ + {/* Left: Scrollable Stacked Content */} +
+ + {[ + { isIntro: true }, + ...Array.from({ length: Math.ceil(features.length / 2) }, (_, i) => ({ + isIntro: false, + items: features.slice(i * 2, i * 2 + 2) + })) + ].map((block, idx, arr) => ( + +
+
+ {block.isIntro ? ( + <> +

+ Who are we? +

+
+

+ We are a small team of passionate developers building an open, accessible, and community-driven namespace for the modern internet. +

+

+ Providing completely free domain names for open-source projects, communities, and individuals to launch ideas without financial barriers. +

+

+ Enjoy fast and simple domain management, robust built-in security with automated abuse prevention, and absolutely zero hidden fees or upsells. +

+
+ + ) : ( +
+ {block.items.map((f, i) => ( +
+
+ +
+
+
+

{f.title}

+

{f.subtitle}

+
+

+ {f.description} +

+
+
+ ))} +
+ )} +
+ {/* White Tail to hide previous content */} + {idx < arr.length - 1 && ( +
+ )} +
+ {idx < arr.length - 1 && ( +
+ )} + + ))} + +
+ + {/* Right: Floating Globe Illustration (Sticky) */} +
+ {/* Heading above the globe */} +
+

+ Connecting the world +

+
+ +
+ {/* Subtle background glow */} +
+ + {/* The Globe */} +
+ }> + { + if (import.meta.env.DEV) console.log("Clicked:", marker.label); + }} + /> + +
+ +
+
+ +
+
+
+ ); +} diff --git a/src/components/network-map-section.jsx b/src/components/network-map-section.jsx new file mode 100644 index 0000000..1ddb696 --- /dev/null +++ b/src/components/network-map-section.jsx @@ -0,0 +1,71 @@ +import { DottedMap } from "./ui/dotted-map"; + +const mapMarkers = [ + // Primary Nameservers + { lat: 40.7128, lng: -74.006, size: 1.5, pulse: true, label: "ns1.stackryze.com β€’ New York" }, + { lat: 50.1109, lng: 8.6821, size: 1.5, pulse: true, label: "ns2.stackryze.com β€’ Germany" }, + { lat: 17.3850, lng: 78.4867, size: 1.5, pulse: true, label: "ns3.stackryze.com β€’ Hyderabad" } +]; + +export function NetworkMapSection() { + return ( +
+
+ +
+

+ Globally Distributed Name Servers +

+

+ Lightning-fast resolution from edge nodes worldwide, ensuring your domain resolves instantly anywhere on the planet. +

+
+ +
+
+ { + if (!marker.label) return null; + return ( + + {/* Pill Background */} + + {/* Label Text */} + + {marker.label} + + + ); + }} + /> +
+
+
+
+ ); +} diff --git a/src/components/sponsors-section.jsx b/src/components/sponsors-section.jsx index 68aa82b..6166bb3 100644 --- a/src/components/sponsors-section.jsx +++ b/src/components/sponsors-section.jsx @@ -32,52 +32,30 @@ export function SponsorsSection() { ]; return ( -
+
-
-

- Sponsored By +
+

+ Trusted By

-

- These industry leaders help us keep Indevs free, open-source, and accessible to everyone. -

-
+
{sponsors.map((sponsor, idx) => ( -
- - {sponsor.name} - - -

- - {sponsor.name} - - {' '}{sponsor.description.replace(sponsor.name + ' ', '')} -

-
+ {sponsor.name} + ))}
diff --git a/src/components/ui/3d-globe.jsx b/src/components/ui/3d-globe.jsx new file mode 100644 index 0000000..9114243 --- /dev/null +++ b/src/components/ui/3d-globe.jsx @@ -0,0 +1,342 @@ +import React, { useEffect, useRef, useState } from "react"; +import * as THREE from "three"; +import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js"; +import { cn } from "@/lib/utils"; + +// ============================================================================ +// Utility Functions +// ============================================================================ + +/** + * Convert latitude/longitude to 3D cartesian coordinates + */ +function latLngToVector3(lat, lng, radius) { + const phi = (90 - lat) * (Math.PI / 180); + const theta = (lng + 180) * (Math.PI / 180); + + const x = -(radius * Math.sin(phi) * Math.cos(theta)); + const z = radius * Math.sin(phi) * Math.sin(theta); + const y = radius * Math.cos(phi); + + return new THREE.Vector3(x, y, z); +} + +// ============================================================================ +// Atmosphere Shader Shaders +// ============================================================================ + +const atmosphereVertexShader = ` + varying vec3 vNormal; + varying vec3 vPosition; + void main() { + vNormal = normalize(normalMatrix * normal); + vPosition = (modelViewMatrix * vec4(position, 1.0)).xyz; + gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); + } +`; + +const atmosphereFragmentShader = ` + uniform vec3 atmosphereColor; + uniform float intensity; + uniform float fresnelPower; + varying vec3 vNormal; + varying vec3 vPosition; + void main() { + float fresnel = pow(1.0 - abs(dot(vNormal, normalize(-vPosition))), fresnelPower); + gl_FragColor = vec4(atmosphereColor, fresnel * intensity); + } +`; + +// ============================================================================ +// Main Globe3D Component +// ============================================================================ + +const defaultConfig = { + radius: 2, + globeColor: "#05050a", + textureUrl: "https://unpkg.com/three-globe@2.31.0/example/img/earth-blue-marble.jpg", + bumpMapUrl: "https://unpkg.com/three-globe@2.31.0/example/img/earth-topology.png", + showAtmosphere: false, + atmosphereColor: "#3b82f6", + atmosphereIntensity: 0.6, + atmosphereBlur: 3.0, + bumpScale: 1, + autoRotateSpeed: 0.4, + enableZoom: false, + enablePan: false, + minDistance: 4, + maxDistance: 12, + ambientIntensity: 0.8, + pointLightIntensity: 2.0, + backgroundColor: null, +}; + +export function Globe3D({ + markers = [], + config = {}, + className, + onMarkerClick: _onMarkerClick, + onMarkerHover: _onMarkerHover, +}) { + const containerRef = useRef(null); + const canvasRef = useRef(null); + const avatarRefs = useRef([]); + const markerTipObjects = useRef([]); + const [_hoveredIndex, _setHoveredIndex] = useState(null); + + const mergedConfig = { ...defaultConfig, ...config }; + + useEffect(() => { + if (!containerRef.current || !canvasRef.current) return; + + const width = containerRef.current.clientWidth || 1; + const height = containerRef.current.clientHeight || 1; + + // 1. Create Scene + const scene = new THREE.Scene(); + + // 2. Camera + const camera = new THREE.PerspectiveCamera(45, width / height, 0.1, 1000); + camera.position.set(0, 0, mergedConfig.radius * 3.2); + + // 3. Renderer + const renderer = new THREE.WebGLRenderer({ + canvas: canvasRef.current, + antialias: true, + alpha: true, + powerPreference: "high-performance", + }); + renderer.setSize(width, height); + renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)); + + // 4. Lighting + const ambientLight = new THREE.AmbientLight(0xffffff, mergedConfig.ambientIntensity); + scene.add(ambientLight); + + const dirLight1 = new THREE.DirectionalLight(0xffffff, mergedConfig.pointLightIntensity); + dirLight1.position.set(mergedConfig.radius * 5, mergedConfig.radius * 3, mergedConfig.radius * 5); + scene.add(dirLight1); + + const dirLight2 = new THREE.DirectionalLight(0x88ccff, mergedConfig.pointLightIntensity * 0.4); + dirLight2.position.set(-mergedConfig.radius * 4, mergedConfig.radius * 2, -mergedConfig.radius * 3); + scene.add(dirLight2); + + // 5. Globe Group (contains globe mesh and markers for rotation) + const globeGroup = new THREE.Group(); + scene.add(globeGroup); + + // Load Textures + const textureLoader = new THREE.TextureLoader(); + textureLoader.crossOrigin = "anonymous"; + + const earthTexture = textureLoader.load(mergedConfig.textureUrl); + earthTexture.colorSpace = THREE.SRGBColorSpace; + const bumpTexture = textureLoader.load(mergedConfig.bumpMapUrl); + + // Globe Mesh + const globeGeometry = new THREE.SphereGeometry(mergedConfig.radius, 64, 64); + const globeMaterial = new THREE.MeshStandardMaterial({ + map: earthTexture, + bumpMap: bumpTexture, + bumpScale: mergedConfig.bumpScale * 0.05, + roughness: 0.8, + metalness: 0.1, + }); + const globeMesh = new THREE.Mesh(globeGeometry, globeMaterial); + globeGroup.add(globeMesh); + + // Atmosphere Mesh + let atmosphereMesh; + if (mergedConfig.showAtmosphere) { + const fresnelPower = 6.0; // Higher power = thinner, softer glow + const atmosphereMaterial = new THREE.ShaderMaterial({ + uniforms: { + atmosphereColor: { value: new THREE.Color(mergedConfig.atmosphereColor) }, + intensity: { value: mergedConfig.atmosphereIntensity }, + fresnelPower: { value: fresnelPower }, + }, + vertexShader: atmosphereVertexShader, + fragmentShader: atmosphereFragmentShader, + side: THREE.BackSide, + transparent: true, + depthWrite: false, + }); + + // Atmosphere is slightly larger than the globe + const atmosphereGeometry = new THREE.SphereGeometry(mergedConfig.radius * 1.04, 64, 32); + atmosphereMesh = new THREE.Mesh(atmosphereGeometry, atmosphereMaterial); + scene.add(atmosphereMesh); + } + + // 6. Build markers and pins inside globeGroup + const tips = []; + const markerGroups = []; + + markers.forEach((marker) => { + const markerGroup = new THREE.Group(); + + const surfacePos = latLngToVector3(marker.lat, marker.lng, mergedConfig.radius * 1.001); + const topPos = latLngToVector3(marker.lat, marker.lng, mergedConfig.radius * 1.16); + + // Pin line + const direction = topPos.clone().sub(surfacePos).normalize(); + const lineHeight = topPos.distanceTo(surfacePos); + const lineCenter = surfacePos.clone().lerp(topPos, 0.5); + + const lineGeometry = new THREE.CylinderGeometry(0.003, 0.003, lineHeight, 8); + const lineMaterial = new THREE.MeshBasicMaterial({ + color: 0x94a3b8, + transparent: true, + opacity: 0.5, + }); + const lineMesh = new THREE.Mesh(lineGeometry, lineMaterial); + + // Orient cylinder + const quaternion = new THREE.Quaternion(); + quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction); + lineMesh.position.copy(lineCenter); + lineMesh.quaternion.copy(quaternion); + markerGroup.add(lineMesh); + + // Cone at surface + const coneGeometry = new THREE.ConeGeometry(0.012, 0.035, 8); + const coneMaterial = new THREE.MeshBasicMaterial({ color: 0xef4444 }); + const coneMesh = new THREE.Mesh(coneGeometry, coneMaterial); + coneMesh.position.copy(surfacePos); + coneMesh.quaternion.copy(quaternion); + markerGroup.add(coneMesh); + + // Tip Object3D (for tracking world coords) + const tipObject = new THREE.Object3D(); + tipObject.position.copy(topPos); + markerGroup.add(tipObject); + tips.push(tipObject); + + globeGroup.add(markerGroup); + markerGroups.push({ lineMaterial, coneMaterial }); + }); + + markerTipObjects.current = tips; + + // 7. OrbitControls + const controls = new OrbitControls(camera, renderer.domElement); + controls.enableDamping = true; + controls.dampingFactor = 0.05; + controls.enablePan = mergedConfig.enablePan; + controls.enableZoom = mergedConfig.enableZoom; + controls.minDistance = mergedConfig.minDistance; + controls.maxDistance = mergedConfig.maxDistance; + controls.rotateSpeed = 0.5; + + // 8. Animation & Render Loop + let animationFrameId; + const tempV = new THREE.Vector3(); + + const animate = () => { + // Auto rotation + if (mergedConfig.autoRotateSpeed > 0) { + globeGroup.rotation.y += (mergedConfig.autoRotateSpeed * 0.005); + } + + controls.update(); + + // Render Three.js scene + renderer.render(scene, camera); + + // Update absolute positioned React avatars + if (containerRef.current) { + const width = containerRef.current.clientWidth; + const height = containerRef.current.clientHeight; + + markers.forEach((marker, index) => { + const el = avatarRefs.current[index]; + const tip = tips[index]; + if (!el || !tip) return; + + // Get world position of the tip + tip.getWorldPosition(tempV); + + // Dot product: positive = facing camera, negative = behind + const markerDir = tempV.clone().normalize(); + const camDir = camera.position.clone().normalize(); + const dot = markerDir.dot(camDir); + + const isVisible = dot > 0.05; // Slightly lax threshold to prevent sudden cuts on the edge + + if (!isVisible) { + el.style.opacity = "0"; + el.style.pointerEvents = "none"; + return; + } + + // Project to 2D + tempV.project(camera); + + const x = (tempV.x * 0.5 + 0.5) * width; + const y = (-(tempV.y * 0.5) + 0.5) * height; + + el.style.opacity = "1"; + el.style.pointerEvents = "auto"; + el.style.transform = `translate3d(${x}px, ${y}px, 0) translate(-50%, -50%)`; + }); + } + + animationFrameId = requestAnimationFrame(animate); + }; + + animate(); + + // 9. Resize Observer + const resizeObserver = new ResizeObserver((entries) => { + for (let entry of entries) { + const w = containerRef.current?.clientWidth || 0; + const h = containerRef.current?.clientHeight || 0; + if (w > 0 && h > 0) { + camera.aspect = w / h; + camera.updateProjectionMatrix(); + renderer.setSize(w, h); + } + } + }); + if (containerRef.current) { + resizeObserver.observe(containerRef.current); + } + + // 10. Cleanups + return () => { + cancelAnimationFrame(animationFrameId); + resizeObserver.disconnect(); + controls.dispose(); + renderer.dispose(); + + // Dispose all Three.js objects in the scene + scene.traverse((object) => { + if (object.geometry) object.geometry.dispose(); + if (object.material) { + if (Array.isArray(object.material)) { + object.material.forEach((material) => material.dispose()); + } else { + object.material.dispose(); + } + } + }); + + earthTexture.dispose(); + bumpTexture.dispose(); + scene.clear(); + }; + }, [markers]); + + return ( +
+ + +
+ ); +} diff --git a/src/components/ui/dialog.jsx b/src/components/ui/dialog.jsx index 852ea55..503ed0d 100644 --- a/src/components/ui/dialog.jsx +++ b/src/components/ui/dialog.jsx @@ -29,7 +29,7 @@ function DialogOverlay({ return { + const sorted = [...points].sort((a, b) => a.y - b.y || a.x - b.x); + const rowMap = new Map(); + let step = 0; + let prevY = Number.NaN; + let prevXInRow = Number.NaN; + + for (const p of sorted) { + if (p.y !== prevY) { + prevY = p.y; + prevXInRow = Number.NaN; + if (!rowMap.has(p.y)) rowMap.set(p.y, rowMap.size); + } + if (!Number.isNaN(prevXInRow)) { + const delta = p.x - prevXInRow; + if (delta > 0) step = step === 0 ? delta : Math.min(step, delta); + } + prevXInRow = p.x; + } + + return { xStep: step || 1, yToRowIndex: rowMap }; + }, [points]); + + return ( + + {points.map((point, index) => { + const rowIndex = yToRowIndex.get(point.y) ?? 0; + const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0; + return ( + + ); + })} + + {processedMarkers.map((marker, index) => { + const rowIndex = yToRowIndex.get(marker.y) ?? 0; + const offsetX = stagger && rowIndex % 2 === 1 ? xStep / 2 : 0; + + const x = marker.x + offsetX; + const y = marker.y; + const r = marker.size ?? dotRadius; + const shouldPulse = pulse + ? marker.pulse !== false + : marker.pulse === true; + const pulseTo = r * 2.8; + + return ( + + + + {shouldPulse ? ( + + + + + + + + + + + ) : null} + + {renderMarkerOverlay?.({ + marker: { ...marker, x, y }, + index, + x, + y, + r, + })} + + ); + })} + + ); +} diff --git a/src/components/ui/input-otp.jsx b/src/components/ui/input-otp.jsx index 3a951b0..eb0c1a8 100644 --- a/src/components/ui/input-otp.jsx +++ b/src/components/ui/input-otp.jsx @@ -21,7 +21,7 @@ function InputOTP({ function InputOTPGroup({ className, ...props }) { return
; } @@ -36,14 +36,14 @@ function InputOTPSlot({ data-slot="input-otp-slot" data-active={isActive} className={cn( - "data-[active=true]:border-ring data-[active=true]:ring-ring/50 data-[active=true]:aria-invalid:ring-destructive/20 dark:data-[active=true]:aria-invalid:ring-destructive/40 aria-invalid:border-destructive data-[active=true]:aria-invalid:border-destructive dark:bg-input/30 border-input relative flex h-9 w-9 items-center justify-center border-y border-r text-sm shadow-xs transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md data-[active=true]:z-10 data-[active=true]:ring-[3px]", + "relative flex h-14 w-12 sm:h-16 sm:w-14 items-center justify-center rounded-[14px] border border-slate-200 dark:border-white/10 bg-white dark:bg-white/5 text-2xl font-bold text-slate-900 dark:text-white shadow-[0_2px_10px_rgba(0,0,0,0.02)] transition-all duration-300 outline-none data-[active=true]:z-10 data-[active=true]:border-slate-900 dark:data-[active=true]:border-white data-[active=true]:ring-4 data-[active=true]:ring-slate-900/10 dark:data-[active=true]:ring-white/10", className )} {...props} > {char} {hasFakeCaret &&
-
+
}
; } diff --git a/src/components/ui/tailwindcss-buttons.jsx b/src/components/ui/tailwindcss-buttons.jsx new file mode 100644 index 0000000..966df3a --- /dev/null +++ b/src/components/ui/tailwindcss-buttons.jsx @@ -0,0 +1,204 @@ + +import { cn } from "@/lib/utils"; + +// 1. Sketch Button - Neobrutalist design with a solid offset border/shadow on hover +export function SketchButton({ children, className, ...props }) { + return ( + + ); +} + +// 2. Simple Button - Elegant transition with subtle translation & shadow on hover +export function SimpleButton({ children, className, ...props }) { + return ( + + ); +} + +// 3. Invert Button - Simple color inversion on hover +export function InvertButton({ children, className, ...props }) { + return ( + + ); +} + +// 4. Tailwind Connect Button - Premium radial gradient, glow overlay, and bottom gradient line +export function TailwindConnectButton({ children, className, ...props }) { + return ( + + ); +} + +// 5. Gradient Button - Premium smooth vertical gradient with hover shadows +export function GradientButton({ children, className, ...props }) { + return ( + + ); +} + +// 6. Unapologetic Button - Solid flat offset shadow box translating on hover +export function UnapologeticButton({ children, className, shadowBg = "bg-yellow-300", ...props }) { + return ( + + ); +} + +// 7. Lit Up Borders Button - Sleek indigo-to-purple gradient border container +export function LitUpBordersButton({ children, className, ...props }) { + return ( + + ); +} + +// 8. Border Magic Button - Spinning conic gradient background light loop +export function BorderMagicButton({ children, className, ...props }) { + return ( + + ); +} + +// 9. Shimmer Button - Subtle shifting reflection animation +export function ShimmerButton({ children, className, ...props }) { + return ( + + ); +} + +// 10. Spotify Button - Vibrant green tracking-widest uppercase hover transform +export function SpotifyButton({ children, className, ...props }) { + return ( + + ); +} + +// 11. NextJS Blue Button - Bright blue shadow-glow transition +export function NextJSBlueButton({ children, className, ...props }) { + return ( + + ); +} + +// 12. NextJS White Button - Pure white card shadow-glow transition +export function NextJSWhiteButton({ children, className, ...props }) { + return ( + + ); +} diff --git a/src/components/ui/toast.jsx b/src/components/ui/toast.jsx index 9aed58e..97613a8 100644 --- a/src/components/ui/toast.jsx +++ b/src/components/ui/toast.jsx @@ -2,7 +2,7 @@ import * as React from "react"; import * as ToastPrimitives from "@radix-ui/react-toast"; import { cva } from "class-variance-authority"; -import { X, CheckCircle, XCircle, AlertCircle, Info } from "lucide-react"; +import { X } from "lucide-react"; import { cn } from "@/lib/utils"; const ToastProvider = ToastPrimitives.Provider; @@ -11,7 +11,7 @@ const ToastViewport = React.forwardRef(({ className, ...props }, ref) => ( ( ToastViewport.displayName = ToastPrimitives.Viewport.displayName; const toastVariants = cva( - "group pointer-events-auto relative flex w-full items-start gap-4 overflow-hidden rounded-lg border-4 p-6 shadow-[8px_8px_0px_0px_rgba(0,0,0,0.9)] transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full", + "group pointer-events-auto relative flex w-full items-start gap-4 overflow-hidden rounded-xl border p-5 shadow-xl dark:shadow-[0_20px_40px_-15px_rgba(0,0,0,0.7)] backdrop-blur-xl transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-bottom-full", { variants: { variant: { - default: "border-[#1A1A1A] bg-white text-[#1A1A1A]", - destructive: "border-red-600 bg-red-50 text-red-900", - success: "border-green-600 bg-green-50 text-green-900", + default: "bg-white/90 dark:bg-[#1A1A1A]/80 border-slate-200/80 dark:border-white/10 text-slate-900 dark:text-white", + destructive: "border-red-500/30 dark:border-red-500/30 bg-red-50/90 dark:bg-red-950/80 text-red-900 dark:text-red-400", + success: "border-emerald-500/30 dark:border-emerald-500/30 bg-emerald-50/90 dark:bg-emerald-950/80 text-emerald-900 dark:text-emerald-400", }, }, defaultVariants: { @@ -51,7 +51,7 @@ const ToastAction = React.forwardRef(({ className, ...props }, ref) => ( ( ( )); @@ -86,7 +86,7 @@ ToastTitle.displayName = ToastPrimitives.Title.displayName; const ToastDescription = React.forwardRef(({ className, ...props }, ref) => ( )); diff --git a/src/index.css b/src/index.css index b3f46f9..03bd1a4 100644 --- a/src/index.css +++ b/src/index.css @@ -1,29 +1,42 @@ @import "tailwindcss"; +@custom-variant dark (&:where(.dark, .dark *)); + @theme { - --color-border: var(--border); - --color-input: var(--input); - --color-ring: var(--ring); - --color-background: var(--background); - --color-foreground: var(--foreground); - --color-primary: var(--primary); - --color-primary-foreground: var(--primary-foreground); - --color-secondary: var(--secondary); - --color-secondary-foreground: var(--secondary-foreground); - --color-destructive: var(--destructive); - --color-destructive-foreground: var(--destructive-foreground); - --color-muted: var(--muted); - --color-muted-foreground: var(--muted-foreground); - --color-accent: var(--accent); - --color-accent-foreground: var(--accent-foreground); - --color-popover: var(--popover); - --color-popover-foreground: var(--popover-foreground); - --color-card: var(--card); - --color-card-foreground: var(--card-foreground); + --color-border: hsl(var(--border)); + --color-input: hsl(var(--input)); + --color-ring: hsl(var(--ring)); + --color-background: hsl(var(--background)); + --color-foreground: hsl(var(--foreground)); + --color-primary: hsl(var(--primary)); + --color-primary-foreground: hsl(var(--primary-foreground)); + --color-secondary: hsl(var(--secondary)); + --color-secondary-foreground: hsl(var(--secondary-foreground)); + --color-destructive: hsl(var(--destructive)); + --color-destructive-foreground: hsl(var(--destructive-foreground)); + --color-muted: hsl(var(--muted)); + --color-muted-foreground: hsl(var(--muted-foreground)); + --color-accent: hsl(var(--accent)); + --color-accent-foreground: hsl(var(--accent-foreground)); + --color-popover: hsl(var(--popover)); + --color-popover-foreground: hsl(var(--popover-foreground)); + --color-card: hsl(var(--card)); + --color-card-foreground: hsl(var(--card-foreground)); --radius-sm: calc(var(--radius) - 4px); --radius-md: calc(var(--radius) - 2px); --radius-lg: var(--radius); + + --animate-shimmer: shimmer 2s linear infinite; + + @keyframes shimmer { + from { + background-position: 0 0; + } + to { + background-position: -200% 0; + } + } } @tailwind base; @@ -136,8 +149,28 @@ textarea { @apply border-border; } + /* Fix browser autofill styling for light mode */ + input:-webkit-autofill, + input:-webkit-autofill:hover, + input:-webkit-autofill:focus, + input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px white inset !important; + -webkit-text-fill-color: #0f172a !important; + transition: background-color 5000s ease-in-out 0s; + } + + /* Fix browser autofill styling for dark mode */ + .dark input:-webkit-autofill, + .dark input:-webkit-autofill:hover, + .dark input:-webkit-autofill:focus, + .dark input:-webkit-autofill:active { + -webkit-box-shadow: 0 0 0 30px #1C1C1C inset !important; + -webkit-text-fill-color: white !important; + transition: background-color 5000s ease-in-out 0s; + } + html { - @apply h-full w-full antialiased bg-[#FFF8F0] text-[#1A1A1A] overflow-x-hidden; + @apply h-full w-full antialiased bg-[#F5F5F5] dark:bg-[#1A1A1A] text-slate-900 dark:text-slate-100 overflow-x-hidden; } body { @@ -157,14 +190,16 @@ textarea { /* Animations */ @keyframes scroll { 0% { - transform: translateX(0); + transform: translate3d(0, 0, 0); } 100% { - transform: translateX(-50%); + transform: translate3d(-50%, 0, 0); } } .animate-scroll { animation: scroll 20s linear infinite; + will-change: transform; + backface-visibility: hidden; } \ No newline at end of file diff --git a/src/layouts/DashboardLayout.jsx b/src/layouts/DashboardLayout.jsx index cd4dd2e..f63f55c 100644 --- a/src/layouts/DashboardLayout.jsx +++ b/src/layouts/DashboardLayout.jsx @@ -24,9 +24,9 @@ const SidebarItem = ({ to, icon: Icon, label, active, onClick }) => ( @@ -103,7 +103,7 @@ export default function DashboardLayout() { active={isActive("/whois")} onClick={() => setSidebarOpen(false)} /> -
+
{/* Analytics placeholder */}
@@ -131,13 +131,13 @@ export default function DashboardLayout() { />
-
+
@@ -174,23 +174,23 @@ export default function DashboardLayout() {
{/* Desktop Sidebar */} -