From 5ce91a65a7fd7aa75a3a87acfbf0c7742a983d9d Mon Sep 17 00:00:00 2001 From: satyakwok <119509589+satyakwok@users.noreply.github.com> Date: Sun, 31 May 2026 22:38:00 +0200 Subject: [PATCH] feat(apps): scaffold register + validators-portal placeholder pages --- apps/register/.env.example | 4 + apps/register/.gitignore | 27 ++ apps/register/README.md | 61 ++++ apps/register/app/globals.css | 278 ++++++++++++++++++ apps/register/app/icon.png | Bin 0 -> 12817 bytes apps/register/app/icon.tsx | 38 +++ apps/register/app/layout.tsx | 83 ++++++ apps/register/app/opengraph-image.tsx | 97 ++++++ apps/register/app/page.tsx | 129 ++++++++ apps/register/app/robots.ts | 10 + apps/register/app/sitemap.ts | 14 + apps/register/components/scroll-reveal.tsx | 29 ++ apps/register/components/theme-provider.tsx | 7 + apps/register/components/theme-toggle.tsx | 39 +++ apps/register/components/ui/eyebrow.tsx | 5 + apps/register/components/ui/logo.tsx | 37 +++ .../components/ui/section-heading.tsx | 32 ++ apps/register/content/nav.ts | 33 +++ apps/register/content/products.ts | 73 +++++ apps/register/content/site.ts | 29 ++ apps/register/content/values.ts | 32 ++ apps/register/eslint.config.mjs | 30 ++ apps/register/globals.d.ts | 9 + apps/register/lib/chain.ts | 54 ++++ apps/register/lib/fonts.ts | 31 ++ apps/register/lib/utils.ts | 14 + apps/register/next.config.ts | 18 ++ apps/register/package.json | 34 +++ apps/register/postcss.config.mjs | 5 + apps/register/tsconfig.json | 34 +++ apps/validators-portal/.env.example | 4 + apps/validators-portal/.gitignore | 27 ++ apps/validators-portal/README.md | 61 ++++ apps/validators-portal/app/globals.css | 278 ++++++++++++++++++ apps/validators-portal/app/icon.png | Bin 0 -> 12817 bytes apps/validators-portal/app/icon.tsx | 38 +++ apps/validators-portal/app/layout.tsx | 83 ++++++ .../validators-portal/app/opengraph-image.tsx | 97 ++++++ apps/validators-portal/app/page.tsx | 134 +++++++++ apps/validators-portal/app/robots.ts | 10 + apps/validators-portal/app/sitemap.ts | 14 + .../components/scroll-reveal.tsx | 29 ++ .../components/theme-provider.tsx | 7 + .../components/theme-toggle.tsx | 39 +++ .../components/ui/eyebrow.tsx | 5 + apps/validators-portal/components/ui/logo.tsx | 37 +++ .../components/ui/section-heading.tsx | 32 ++ apps/validators-portal/content/nav.ts | 33 +++ apps/validators-portal/content/products.ts | 73 +++++ apps/validators-portal/content/site.ts | 30 ++ apps/validators-portal/content/values.ts | 32 ++ apps/validators-portal/eslint.config.mjs | 30 ++ apps/validators-portal/globals.d.ts | 9 + apps/validators-portal/lib/chain.ts | 54 ++++ apps/validators-portal/lib/fonts.ts | 31 ++ apps/validators-portal/lib/utils.ts | 14 + apps/validators-portal/next.config.ts | 18 ++ apps/validators-portal/package.json | 34 +++ apps/validators-portal/postcss.config.mjs | 5 + apps/validators-portal/tsconfig.json | 34 +++ pnpm-lock.yaml | 116 ++++++++ 61 files changed, 2694 insertions(+) create mode 100644 apps/register/.env.example create mode 100644 apps/register/.gitignore create mode 100644 apps/register/README.md create mode 100644 apps/register/app/globals.css create mode 100644 apps/register/app/icon.png create mode 100644 apps/register/app/icon.tsx create mode 100644 apps/register/app/layout.tsx create mode 100644 apps/register/app/opengraph-image.tsx create mode 100644 apps/register/app/page.tsx create mode 100644 apps/register/app/robots.ts create mode 100644 apps/register/app/sitemap.ts create mode 100644 apps/register/components/scroll-reveal.tsx create mode 100644 apps/register/components/theme-provider.tsx create mode 100644 apps/register/components/theme-toggle.tsx create mode 100644 apps/register/components/ui/eyebrow.tsx create mode 100644 apps/register/components/ui/logo.tsx create mode 100644 apps/register/components/ui/section-heading.tsx create mode 100644 apps/register/content/nav.ts create mode 100644 apps/register/content/products.ts create mode 100644 apps/register/content/site.ts create mode 100644 apps/register/content/values.ts create mode 100644 apps/register/eslint.config.mjs create mode 100644 apps/register/globals.d.ts create mode 100644 apps/register/lib/chain.ts create mode 100644 apps/register/lib/fonts.ts create mode 100644 apps/register/lib/utils.ts create mode 100644 apps/register/next.config.ts create mode 100644 apps/register/package.json create mode 100644 apps/register/postcss.config.mjs create mode 100644 apps/register/tsconfig.json create mode 100644 apps/validators-portal/.env.example create mode 100644 apps/validators-portal/.gitignore create mode 100644 apps/validators-portal/README.md create mode 100644 apps/validators-portal/app/globals.css create mode 100644 apps/validators-portal/app/icon.png create mode 100644 apps/validators-portal/app/icon.tsx create mode 100644 apps/validators-portal/app/layout.tsx create mode 100644 apps/validators-portal/app/opengraph-image.tsx create mode 100644 apps/validators-portal/app/page.tsx create mode 100644 apps/validators-portal/app/robots.ts create mode 100644 apps/validators-portal/app/sitemap.ts create mode 100644 apps/validators-portal/components/scroll-reveal.tsx create mode 100644 apps/validators-portal/components/theme-provider.tsx create mode 100644 apps/validators-portal/components/theme-toggle.tsx create mode 100644 apps/validators-portal/components/ui/eyebrow.tsx create mode 100644 apps/validators-portal/components/ui/logo.tsx create mode 100644 apps/validators-portal/components/ui/section-heading.tsx create mode 100644 apps/validators-portal/content/nav.ts create mode 100644 apps/validators-portal/content/products.ts create mode 100644 apps/validators-portal/content/site.ts create mode 100644 apps/validators-portal/content/values.ts create mode 100644 apps/validators-portal/eslint.config.mjs create mode 100644 apps/validators-portal/globals.d.ts create mode 100644 apps/validators-portal/lib/chain.ts create mode 100644 apps/validators-portal/lib/fonts.ts create mode 100644 apps/validators-portal/lib/utils.ts create mode 100644 apps/validators-portal/next.config.ts create mode 100644 apps/validators-portal/package.json create mode 100644 apps/validators-portal/postcss.config.mjs create mode 100644 apps/validators-portal/tsconfig.json diff --git a/apps/register/.env.example b/apps/register/.env.example new file mode 100644 index 0000000..2c4940a --- /dev/null +++ b/apps/register/.env.example @@ -0,0 +1,4 @@ +# Sentrix Chain mainnet RPC — used by Stats section to fetch live block data. +# Defaults to the public production endpoint if unset. +NEXT_PUBLIC_MAINNET_RPC=https://rpc.sentrixchain.com +NEXT_PUBLIC_MAINNET_CHAIN_ID=7119 diff --git a/apps/register/.gitignore b/apps/register/.gitignore new file mode 100644 index 0000000..9125795 --- /dev/null +++ b/apps/register/.gitignore @@ -0,0 +1,27 @@ +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env +.env*.local +.env + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/register/README.md b/apps/register/README.md new file mode 100644 index 0000000..0d679c4 --- /dev/null +++ b/apps/register/README.md @@ -0,0 +1,61 @@ +# @sentriscloud/landing + +Company landing page for **sentriscloud.com**. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript +- Tailwind CSS 4 (CSS-first config in `app/globals.css`) +- next-themes (dark default) +- framer-motion (scroll reveals) +- viem (live mainnet stats) +- lucide-react (icons) + +## Editing copy + +All user-facing copy lives in `content/`: + +| File | Purpose | +| --- | --- | +| `content/site.ts` | Site name, tagline, description, social URLs, brand asset URLs | +| `content/products.ts` | Product cards: name, tagline, description, status, href | +| `content/values.ts` | "Why SentrisCloud" value props | +| `content/nav.ts` | Top nav and footer link grids | + +Editing copy never touches JSX — change the data, the section re-renders automatically. + +## Adding a section + +1. Create `components/sections/.tsx` +2. Export a function component +3. Import in `app/page.tsx` and place it where you want +4. If it has data, put data in `content/.ts` + +## Live stats + +`components/sections/stats.tsx` is a server component that calls `getChainSnapshot()` (in `lib/chain.ts`) which queries the mainnet RPC. The whole page is revalidated every 60s (`export const revalidate = 60` in `app/page.tsx`) — keeps RPC traffic predictable. + +Override the RPC by setting `NEXT_PUBLIC_MAINNET_RPC` in `.env.local`. + +## Run + +From the monorepo root: + +```bash +pnpm -F @sentriscloud/landing dev +# → http://localhost:3000 +``` + +Build: + +```bash +pnpm -F @sentriscloud/landing build +``` + +## Deploy + +Vercel-ready: `Root Directory` = `apps/landing`, `Install Command` = `pnpm install`, `Build Command` = `pnpm build`. Set `NEXT_PUBLIC_MAINNET_RPC` if not using the default. + +## Design system + +Aesthetic follows `sentris-design`: editorial luxury (Playfair Display + Sora + IBM Plex Mono, emerald-on-canvas dark), no crypto-neon, no glassmorphism. Tokens defined in `app/globals.css` under `@theme`. diff --git a/apps/register/app/globals.css b/apps/register/app/globals.css new file mode 100644 index 0000000..0800b65 --- /dev/null +++ b/apps/register/app/globals.css @@ -0,0 +1,278 @@ +@import "tailwindcss"; + +/* CSS custom properties — defined outside @theme so Tailwind 4 doesn't + strip them during compile (var() indirection inside @theme is unreliable). */ +:root { + /* Brand family — emerald shared with Sentrix Labs */ + --color-emerald-50: #ecfdf5; + --color-emerald-100: #d1fae5; + --color-emerald-300: #6ee7b7; + --color-emerald-400: #34d399; + --color-emerald-500: #10b981; + --color-emerald-600: #059669; + --color-emerald-700: #047857; + --color-emerald-900: #064e3b; + + --color-bronze: #8a5a11; + --color-gold: #dbc17f; + + --color-canvas: #0a0a0c; + --color-canvas-2: #111114; + --color-ink: #f5f5f4; + --color-ink-2: #d6d3d1; + --color-ink-3: #a8a29e; + --color-ink-4: #78716c; + --color-line: #27272a; + --color-line-2: #1c1c1f; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out-quart: cubic-bezier(0.76, 0, 0.24, 1); + + color-scheme: dark; +} + +@layer base { + html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + scroll-behavior: smooth; + font-synthesis: none; + } + + body { + background: var(--color-canvas); + color: var(--color-ink); + font-family: var(--font-sora), ui-sans-serif, system-ui, sans-serif; + font-feature-settings: "ss01", "cv11", "kern", "liga"; + font-optical-sizing: auto; + font-variant-ligatures: common-ligatures contextual; + overflow-x: hidden; + } + + ::selection { + background: var(--color-emerald-500); + color: var(--color-canvas); + } + + ::-webkit-scrollbar { width: 10px; height: 10px; } + ::-webkit-scrollbar-track { background: var(--color-canvas); } + ::-webkit-scrollbar-thumb { + background: var(--color-line); + border: 2px solid var(--color-canvas); + border-radius: 5px; + } + + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } +} + +@layer components { + /* Editorial display heading — Fraunces, tuned for tech-editorial. + opsz=144 (display optical size, tighter terminals). + SOFT=50 (slightly humanized — less luxury-bridal, more modern). + No discretionary ligatures — those swashes read as "alay" in a + tech context; we want refined, not decorative. */ + .display { + font-family: var(--font-display), ui-serif, Georgia, "Times New Roman", serif; + font-weight: 500; + font-variation-settings: "opsz" 36; + letter-spacing: -0.025em; + line-height: 0.95; + font-feature-settings: "lnum", "kern", "liga", "calt"; + font-variant-ligatures: common-ligatures contextual; + font-optical-sizing: auto; + text-wrap: balance; + } + + /* Accent variant — same upright serif as .display, slightly heavier + so emerald color + weight bump carry the emphasis instead of italic + (italic serif reads "alay" / calligraphic in a tech context). */ + .display-italic { + font-family: var(--font-display), ui-serif, Georgia, "Times New Roman", serif; + font-style: normal; + font-weight: 600; + font-variation-settings: "opsz" 36; + letter-spacing: -0.026em; + line-height: 0.95; + font-feature-settings: "kern", "liga", "calt"; + font-variant-ligatures: common-ligatures contextual; + font-optical-sizing: auto; + } + + /* Heavy emphasis — Fraunces 700, still optical-display sized. + 800 was too bridal; 700 keeps presence without theatrics. */ + .display-heavy { + font-family: var(--font-display), ui-serif, Georgia, "Times New Roman", serif; + font-weight: 700; + font-variation-settings: "opsz" 36; + letter-spacing: -0.028em; + line-height: 0.92; + font-feature-settings: "lnum", "kern", "liga", "calt"; + font-variant-ligatures: common-ligatures contextual; + font-optical-sizing: auto; + text-wrap: balance; + } + + /* Mono — for code, data, eyebrows. Slashed zero + tabular figures so + numbers don't shimmy when they update live. */ + .mono { + font-family: var(--font-plex-mono), ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; + font-feature-settings: "tnum", "zero", "ss01", "calt"; + font-variant-numeric: tabular-nums slashed-zero; + } + + /* Section number tag — "01 / Hero", editorial cue */ + .section-number { + font-family: var(--font-plex-mono), ui-monospace, monospace; + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--color-ink-4); + } + + .eyebrow { + font-family: var(--font-plex-mono), ui-monospace, monospace; + font-size: 0.6875rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--color-emerald-500); + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .eyebrow::before { + content: ""; + display: block; + width: 1.5rem; + height: 1px; + background: currentColor; + opacity: 0.5; + } + + .container-page { + width: 100%; + max-width: 1280px; + margin-inline: auto; + padding-inline: 1.5rem; + } + + @media (min-width: 768px) { + .container-page { padding-inline: 2.5rem; } + } + + /* Pull rule — full-width hairline divider */ + .rule { + border: 0; + border-top: 1px solid var(--color-line); + margin: 0; + } + + /* Subtle grain texture overlay */ + .grain::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + opacity: 0.025; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + mix-blend-mode: overlay; + } + + /* Drop-cap (used in the manifesto-style sections) */ + .dropcap::first-letter { + font-family: var(--font-display), ui-serif, serif; + font-size: 4.5em; + line-height: 0.85; + float: left; + margin: 0.05em 0.08em -0.1em 0; + color: var(--color-emerald-500); + font-weight: 500; + } + + /* Corner-lines — signature SentrisCloud card treatment. Four small gold + accents flick to visible on hover, framing the content like editorial + bracketing. */ + .corner-lines { + position: relative; + } + + .corner-lines::before, + .corner-lines::after, + .corner-lines > .cl-bl, + .corner-lines > .cl-br { + content: ""; + position: absolute; + width: 18px; + height: 18px; + border: 1px solid var(--color-gold); + opacity: 0; + transition: opacity 350ms var(--ease-out-expo); + pointer-events: none; + } + + .corner-lines::before { + top: 0; left: 0; + border-right: none; border-bottom: none; + } + + .corner-lines::after { + top: 0; right: 0; + border-left: none; border-bottom: none; + } + + .corner-lines > .cl-bl { + bottom: 0; left: 0; + border-right: none; border-top: none; + } + + .corner-lines > .cl-br { + bottom: 0; right: 0; + border-left: none; border-top: none; + } + + .corner-lines:hover::before, + .corner-lines:hover::after, + .corner-lines:hover > .cl-bl, + .corner-lines:hover > .cl-br { + opacity: 0.85; + } + + /* Oversized backdrop numeral — used behind hero for editorial drama. + Upright (italic numerals get gimmicky); use Newsreader at low + weight so it reads as a quiet structural accent. */ + .backdrop-numeral { + font-family: var(--font-display), ui-serif, serif; + font-style: normal; + font-weight: 400; + font-variation-settings: "opsz" 36; + color: var(--color-canvas-2); + line-height: 0.8; + letter-spacing: -0.05em; + user-select: none; + pointer-events: none; + } + + /* Animated underline link */ + .link-underline { + background-image: linear-gradient(currentColor, currentColor); + background-size: 0 1px; + background-position: 0 100%; + background-repeat: no-repeat; + transition: background-size 300ms var(--ease-out-expo); + } + + .link-underline:hover { + background-size: 100% 1px; + } +} diff --git a/apps/register/app/icon.png b/apps/register/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3137a95822b55aea70090d95ddfff0524eb1ab GIT binary patch literal 12817 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 + + + + + + + + + ), + { ...size }, + ); +} diff --git a/apps/register/app/layout.tsx b/apps/register/app/layout.tsx new file mode 100644 index 0000000..cd93578 --- /dev/null +++ b/apps/register/app/layout.tsx @@ -0,0 +1,83 @@ +import type { Metadata, Viewport } from "next"; + +import { site } from "@/content/site"; +import { display, sora, plexMono } from "@/lib/fonts"; +import { cn } from "@/lib/utils"; +import { ThemeProvider } from "@/components/theme-provider"; + +import "./globals.css"; + +export const metadata: Metadata = { + metadataBase: new URL(site.url), + title: { + default: `${site.name} — ${site.tagline}`, + template: `%s · ${site.name}`, + }, + description: site.description, + keywords: [ + "Sentrix Chain", + "SentrisCloud", + "Layer 1 blockchain", + "block explorer", + "wallet", + "faucet", + "DEX", + "SRX", + ], + authors: [{ name: site.name }], + creator: site.name, + openGraph: { + type: "website", + url: site.url, + siteName: site.name, + title: `${site.name} — ${site.tagline}`, + description: site.description, + images: [ + { + url: "/opengraph-image", + width: 1200, + height: 630, + alt: `${site.name} — ${site.tagline}`, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: `${site.name} — ${site.tagline}`, + description: site.description, + images: ["/opengraph-image"], + }, + robots: { + index: true, + follow: true, + googleBot: { index: true, follow: true, "max-image-preview": "large" }, + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: dark)", color: "#0a0a0c" }, + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + ], + width: "device-width", + initialScale: 1, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
{children}
+
+ + + ); +} diff --git a/apps/register/app/opengraph-image.tsx b/apps/register/app/opengraph-image.tsx new file mode 100644 index 0000000..3fb15db --- /dev/null +++ b/apps/register/app/opengraph-image.tsx @@ -0,0 +1,97 @@ +import { ImageResponse } from "next/og"; +import { site } from "@/content/site"; + +export const alt = `${site.name} — ${site.tagline}`; +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +export default function OpengraphImage() { + return new ImageResponse( + ( +
+ {/* Emerald glow accent */} +
+ +
+ + + + + + + + SentrisCloud +
+ +
+ + ── Products built on Sentrix Chain + +

+ The user-facing layer of the Sentrix ecosystem. +

+
+ +
+ SentrixScan · Wallet · Faucet · CoinBlast + sentriscloud.com +
+
+ ), + { ...size }, + ); +} diff --git a/apps/register/app/page.tsx b/apps/register/app/page.tsx new file mode 100644 index 0000000..1edd56f --- /dev/null +++ b/apps/register/app/page.tsx @@ -0,0 +1,129 @@ +import Link from "next/link"; +import { ArrowUpRight } from "lucide-react"; + +import { site } from "@/content/site"; + +export const revalidate = 3600; + +export default function HomePage() { + return ( +
+
+ reg +
+ +
+
+ + +
+

+ Coming soon · Beta in {new Date().getFullYear()} +

+

+ Register a +
+ Sentrix validator{" "} + + from your wallet. + +

+ +
+
+

+ This page will host a web wizard that walks you through + keystore generation, bonding ≥ 15,000 SRX, and broadcasting + the StakingOp::RegisterValidator{" "} + transaction with one wallet click. +

+

+ Until it ships, the CLI path works today and is what active + operators use. The deep guide covers hardware spec, systemd + unit, keystore handling, and monitoring end-to-end. +

+
+ + +
+ +
+ + Read the deep guide + + + + Join waitlist + + + + See active validators + + +
+
+
+ +
+
+

© Sentrix Labs · permissionless validator set

+
+ + sentrixchain.com + + + docs + + + scan + + + {site.email.contact} + +
+
+
+
+
+ ); +} diff --git a/apps/register/app/robots.ts b/apps/register/app/robots.ts new file mode 100644 index 0000000..aa4c5ed --- /dev/null +++ b/apps/register/app/robots.ts @@ -0,0 +1,10 @@ +import type { MetadataRoute } from "next"; +import { site } from "@/content/site"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: [{ userAgent: "*", allow: "/" }], + sitemap: `${site.url}/sitemap.xml`, + host: site.url, + }; +} diff --git a/apps/register/app/sitemap.ts b/apps/register/app/sitemap.ts new file mode 100644 index 0000000..6d0d863 --- /dev/null +++ b/apps/register/app/sitemap.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from "next"; +import { site } from "@/content/site"; + +export default function sitemap(): MetadataRoute.Sitemap { + const lastModified = new Date(); + return [ + { + url: site.url, + lastModified, + changeFrequency: "weekly", + priority: 1, + }, + ]; +} diff --git a/apps/register/components/scroll-reveal.tsx b/apps/register/components/scroll-reveal.tsx new file mode 100644 index 0000000..7907e01 --- /dev/null +++ b/apps/register/components/scroll-reveal.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { motion, type HTMLMotionProps } from "framer-motion"; +import type { ReactNode } from "react"; + +type Props = HTMLMotionProps<"div"> & { + children: ReactNode; + delay?: number; +}; + +/** + * Subtle in-view reveal. Starts at opacity 0.6 (still readable) and animates + * to 1 — so SSR / no-JS content remains visible, and the generic + * marketing-spam reveal feel is dialled down. Don't use on hero / + * above-the-fold content. + */ +export function ScrollReveal({ children, delay = 0, ...rest }: Props) { + return ( + + {children} + + ); +} diff --git a/apps/register/components/theme-provider.tsx b/apps/register/components/theme-provider.tsx new file mode 100644 index 0000000..e478fc4 --- /dev/null +++ b/apps/register/components/theme-provider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/apps/register/components/theme-toggle.tsx b/apps/register/components/theme-toggle.tsx new file mode 100644 index 0000000..54d922b --- /dev/null +++ b/apps/register/components/theme-toggle.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useSyncExternalStore } from "react"; + +// useSyncExternalStore-based mount detection — equivalent to the classic +// useState+useEffect pattern but lint-clean under React 19's +// react-hooks/set-state-in-effect rule. Server snapshot is false, client +// resolves true on subscribe — same hydration-safe behavior. +const subscribeMount = () => () => {}; +const getMountSnapshot = () => true; +const getMountServerSnapshot = () => false; + +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + const mounted = useSyncExternalStore( + subscribeMount, + getMountSnapshot, + getMountServerSnapshot, + ); + + if (!mounted) { + return
; + } + + const isDark = resolvedTheme === "dark"; + + return ( + + ); +} diff --git a/apps/register/components/ui/eyebrow.tsx b/apps/register/components/ui/eyebrow.tsx new file mode 100644 index 0000000..ed7e9ba --- /dev/null +++ b/apps/register/components/ui/eyebrow.tsx @@ -0,0 +1,5 @@ +import { cn } from "@/lib/utils"; + +export function Eyebrow({ children, className }: { children: React.ReactNode; className?: string }) { + return {children}; +} diff --git a/apps/register/components/ui/logo.tsx b/apps/register/components/ui/logo.tsx new file mode 100644 index 0000000..2641461 --- /dev/null +++ b/apps/register/components/ui/logo.tsx @@ -0,0 +1,37 @@ +import { cn } from "@/lib/utils"; + +type Props = { + className?: string; + size?: number; + withWordmark?: boolean; +}; + +/** + * SentrisCloud mark — 5-dot quincunx in family emerald. + * Inlined SVG (no jsDelivr fetch) for above-the-fold render. + */ +export function Logo({ className, size = 28, withWordmark = false }: Props) { + return ( + + + + + + + + + {withWordmark ? ( + SentrisCloud + ) : null} + + ); +} diff --git a/apps/register/components/ui/section-heading.tsx b/apps/register/components/ui/section-heading.tsx new file mode 100644 index 0000000..32ea168 --- /dev/null +++ b/apps/register/components/ui/section-heading.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils"; +import { Eyebrow } from "./eyebrow"; + +type Props = { + eyebrow?: string; + title: string; + description?: string; + className?: string; + align?: "left" | "center"; +}; + +export function SectionHeading({ eyebrow, title, description, className, align = "left" }: Props) { + return ( +
+ {eyebrow ? {eyebrow} : null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ); +} diff --git a/apps/register/content/nav.ts b/apps/register/content/nav.ts new file mode 100644 index 0000000..be0fb61 --- /dev/null +++ b/apps/register/content/nav.ts @@ -0,0 +1,33 @@ +export type NavLink = { + label: string; + href: string; + external?: boolean; +}; + +export const primaryNav: NavLink[] = [ + { label: "Products", href: "#products" }, + { label: "Why us", href: "#why" }, + { label: "Developers", href: "#developers" }, + { label: "Chain", href: "https://sentrixchain.com", external: true }, +]; + +export const footerLinks = { + products: [ + { label: "SentrixScan", href: "https://scan.sentrixchain.com", external: true }, + { label: "Solux", href: "https://solux.sentriscloud.com", external: true }, + { label: "Sentrix Faucet", href: "https://faucet.sentrixchain.com", external: true }, + { label: "CoinBlast", href: "https://coinblast.sentriscloud.com", external: true }, + ], + ecosystem: [ + { label: "Sentrix Chain", href: "https://sentrixchain.com", external: true }, + { label: "Sentrix Labs", href: "https://github.com/sentrix-labs", external: true }, + { label: "Brand kit", href: "https://github.com/sentrix-labs/brand-kit", external: true }, + { label: "Validators", href: "https://scan.sentrixchain.com/validators", external: true }, + ], + company: [ + { label: "About", href: "#about" }, + { label: "Contact", href: "mailto:contact@sentriscloud.com" }, + { label: "Security", href: "mailto:security@sentriscloud.com" }, + { label: "GitHub", href: "https://github.com/Sentriscloud", external: true }, + ], +} as const; diff --git a/apps/register/content/products.ts b/apps/register/content/products.ts new file mode 100644 index 0000000..706c7f6 --- /dev/null +++ b/apps/register/content/products.ts @@ -0,0 +1,73 @@ +/** + * Products shown in the products grid. + * Edit copy / add new products here — components re-render automatically. + */ +import type { LucideIcon } from "lucide-react"; +import { Search, Wallet, Droplets, TrendingUp } from "lucide-react"; + +export type ProductStatus = "live" | "beta" | "in-development" | "planned"; + +export type Product = { + slug: string; + name: string; + tagline: string; + description: string; + href: string; + status: ProductStatus; + icon: LucideIcon; + /** External — opens in new tab. */ + external?: boolean; +}; + +export const products = [ + { + slug: "scan", + name: "SentrixScan", + tagline: "Block explorer for Sentrix Chain", + description: + "Browse blocks, transactions, addresses, validators, and SRC-20 tokens. Real-time stats, smart search, mainnet + testnet.", + href: "https://scan.sentrixchain.com", + status: "live", + icon: Search, + external: true, + }, + { + slug: "solux", + name: "Solux", + tagline: "Self-custody wallet for SRX", + description: + "Send, receive, and manage SRX and SRC-20 tokens. Web today, mobile (Flutter) in development. Same brand, same keys, two surfaces.", + href: "https://solux.sentriscloud.com", + status: "in-development", + icon: Wallet, + external: true, + }, + { + slug: "faucet", + name: "Sentrix Faucet", + tagline: "Testnet token tap for builders", + description: + "Request testnet SRX in seconds. Rate-limited, no signup, ready for CI and integration testing.", + href: "https://faucet.sentrixchain.com", + status: "live", + icon: Droplets, + external: true, + }, + { + slug: "coinblast", + name: "CoinBlast", + tagline: "DEX + token launchpad", + description: + "Fair-launch tooling, bonding curves, and an integrated DEX for the Sentrix ecosystem. Coming soon.", + href: "#", + status: "planned", + icon: TrendingUp, + }, +] as const satisfies readonly Product[]; + +export const statusLabels: Record = { + live: "Live", + beta: "Beta", + "in-development": "In development", + planned: "Planned", +}; diff --git a/apps/register/content/site.ts b/apps/register/content/site.ts new file mode 100644 index 0000000..8dd9ea3 --- /dev/null +++ b/apps/register/content/site.ts @@ -0,0 +1,29 @@ +/** + * Site-wide metadata + branding constants. + * Edit here once; pulled by layout, OG image, sitemap, robots. + */ +export const site = { + name: "Sentrix Validator Registration", + tagline: "Self-service validator onboarding for Sentrix Chain.", + description: + "Web wizard for submitting StakingOp::RegisterValidator with WalletConnect — coming soon. CLI path (`sentrix staking register`) is available today.", + url: "https://register.sentrixchain.com", + email: { + contact: "validators@sentrixchain.com", + security: "security@sentriscloud.com", + }, + social: { + github: "https://github.com/sentrix-labs/sentrix", + twitter: "https://x.com/sentriscloud", + telegram: "https://t.me/SentrixChain", + }, + related: { + chain: "https://sentrixchain.com", + docs: "https://docs.sentrixchain.com/operations/validator-onboarding", + scan: "https://scan.sentrixchain.com/validators", + }, + marks: { + logoPng: "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/png-transparent/sentriscloud-512.png", + logoSvg: "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/svg/sentriscloud-mark.svg", + }, +} as const; diff --git a/apps/register/content/values.ts b/apps/register/content/values.ts new file mode 100644 index 0000000..2008bc1 --- /dev/null +++ b/apps/register/content/values.ts @@ -0,0 +1,32 @@ +/** + * "Why SentrisCloud" value props. Edit copy here. + */ +import type { LucideIcon } from "lucide-react"; +import { Layers, ShieldCheck, Users } from "lucide-react"; + +export type Value = { + title: string; + description: string; + icon: LucideIcon; +}; + +export const values: Value[] = [ + { + title: "One ecosystem, four surfaces", + description: + "Every product shares chain primitives — wallet, explorer, faucet, exchange — so the developer story stays coherent and the user experience stays familiar across them.", + icon: Layers, + }, + { + title: "Built on Sentrix Chain", + description: + "Native Layer 1 with sub-second blocks and instant finality. We don't ride someone else's settlement layer — we operate the protocol our products depend on.", + icon: ShieldCheck, + }, + { + title: "Open to validators and builders", + description: + "External validators onboarding in 2026. SDKs, brand assets, and tooling are public. We grow the network, not just the product.", + icon: Users, + }, +]; diff --git a/apps/register/eslint.config.mjs b/apps/register/eslint.config.mjs new file mode 100644 index 0000000..4bdf495 --- /dev/null +++ b/apps/register/eslint.config.mjs @@ -0,0 +1,30 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([ + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), + { + rules: { + // TODO: re-enable once the React 19 / react-compiler refactor lands. + // The eslint-plugin-react-hooks v6 (pulled in via eslint-config-next 16) + // ships new compiler-aware rules that flag ~30 violations across the + // monorepo — every one is a real refactor, not a quick fix. + "react-hooks/preserve-manual-memoization": "off", + "react-hooks/purity": "off", + "react-hooks/refs": "off", + "react-hooks/set-state-in-effect": "off", + "react-hooks/static-components": "off", + "react-hooks/use-memo": "off", + }, + }, +]); + +export default eslintConfig; diff --git a/apps/register/globals.d.ts b/apps/register/globals.d.ts new file mode 100644 index 0000000..6ec1735 --- /dev/null +++ b/apps/register/globals.d.ts @@ -0,0 +1,9 @@ +/// +/// + +// CSS side-effect imports (e.g. `import "./globals.css"`) +// Typically declared by the auto-generated next-env.d.ts, but that +// file is gitignored + only emitted after `next build`. CI runs +// `pnpm typecheck` before `next build`, so we mirror those refs +// here to keep TS 6 happy. +declare module "*.css"; diff --git a/apps/register/lib/chain.ts b/apps/register/lib/chain.ts new file mode 100644 index 0000000..d41dcbf --- /dev/null +++ b/apps/register/lib/chain.ts @@ -0,0 +1,54 @@ +import { createPublicClient, http, type Chain } from "viem"; + +const MAINNET_RPC = process.env.NEXT_PUBLIC_MAINNET_RPC ?? "https://rpc.sentrixchain.com"; +const MAINNET_CHAIN_ID = Number(process.env.NEXT_PUBLIC_MAINNET_CHAIN_ID ?? 7119); + +export const sentrixMainnet: Chain = { + id: MAINNET_CHAIN_ID, + name: "Sentrix Chain", + nativeCurrency: { name: "Sentrix", symbol: "SRX", decimals: 18 }, + rpcUrls: { + default: { http: [MAINNET_RPC] }, + }, + blockExplorers: { + default: { name: "Sentrix Scan", url: "https://scan.sentrixchain.com" }, + }, +}; + +export const publicClient = createPublicClient({ + chain: sentrixMainnet, + transport: http(MAINNET_RPC, { timeout: 5_000 }), +}); + +export type ChainSnapshot = { + blockHeight: number; + blockTime: number; + validatorCount: number | null; + status: "live" | "stale"; + fetchedAt: number; +}; + +export async function getChainSnapshot(): Promise { + try { + const [blockNumber, block] = await Promise.all([ + publicClient.getBlockNumber(), + publicClient.getBlock({ blockTag: "latest" }), + ]); + + return { + blockHeight: Number(blockNumber), + blockTime: Number(block.timestamp), + validatorCount: null, + status: "live", + fetchedAt: Date.now(), + }; + } catch { + return { + blockHeight: 0, + blockTime: 0, + validatorCount: null, + status: "stale", + fetchedAt: Date.now(), + }; + } +} diff --git a/apps/register/lib/fonts.ts b/apps/register/lib/fonts.ts new file mode 100644 index 0000000..b09a1a9 --- /dev/null +++ b/apps/register/lib/fonts.ts @@ -0,0 +1,31 @@ +import { Newsreader, Sora, IBM_Plex_Mono } from "next/font/google"; + +/** + * Editorial display — Newsreader. + * News-grade serif with optical sizing (opsz axis). Italic is restrained + * (true italic, not script-y swashes), which is what we want for + * "editorial tech" rather than "luxury bridal magazine". Variable so we + * use one file for the whole weight + opsz + italic range. + */ +export const display = Newsreader({ + subsets: ["latin"], + display: "swap", + variable: "--font-display", + weight: "variable", + style: ["normal", "italic"], + axes: ["opsz"], +}); + +export const sora = Sora({ + subsets: ["latin"], + display: "swap", + variable: "--font-sora", + weight: ["200", "300", "400", "500", "600", "700"], +}); + +export const plexMono = IBM_Plex_Mono({ + subsets: ["latin"], + display: "swap", + variable: "--font-plex-mono", + weight: ["400", "500"], +}); diff --git a/apps/register/lib/utils.ts b/apps/register/lib/utils.ts new file mode 100644 index 0000000..f7cfc31 --- /dev/null +++ b/apps/register/lib/utils.ts @@ -0,0 +1,14 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatNumber(n: number, opts?: Intl.NumberFormatOptions): string { + return new Intl.NumberFormat("en-US", opts).format(n); +} + +export function formatCompact(n: number): string { + return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 2 }).format(n); +} diff --git a/apps/register/next.config.ts b/apps/register/next.config.ts new file mode 100644 index 0000000..a48b24c --- /dev/null +++ b/apps/register/next.config.ts @@ -0,0 +1,18 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + poweredByHeader: false, + compress: true, + images: { + formats: ["image/avif", "image/webp"], + remotePatterns: [ + { protocol: "https", hostname: "cdn.jsdelivr.net" }, + { protocol: "https", hostname: "raw.githubusercontent.com" }, + ], + }, + experimental: { + optimizePackageImports: ["lucide-react"], + }, +}; + +export default nextConfig; diff --git a/apps/register/package.json b/apps/register/package.json new file mode 100644 index 0000000..998ea28 --- /dev/null +++ b/apps/register/package.json @@ -0,0 +1,34 @@ +{ + "name": "@sentriscloud/register", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "eslint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "clsx": "^2.1.1", + "framer-motion": "^12.38.0", + "lucide-react": "^1.14.0", + "next": "15.5.18", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.5.0", + "viem": "^2.48.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^25", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.6", + "tailwindcss": "^4", + "typescript": "^6" + } +} diff --git a/apps/register/postcss.config.mjs b/apps/register/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/apps/register/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/apps/register/tsconfig.json b/apps/register/tsconfig.json new file mode 100644 index 0000000..616b0b8 --- /dev/null +++ b/apps/register/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "noEmit": true, + "verbatimModuleSyntax": false, + "noUncheckedIndexedAccess": false, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "globals.d.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/apps/validators-portal/.env.example b/apps/validators-portal/.env.example new file mode 100644 index 0000000..2c4940a --- /dev/null +++ b/apps/validators-portal/.env.example @@ -0,0 +1,4 @@ +# Sentrix Chain mainnet RPC — used by Stats section to fetch live block data. +# Defaults to the public production endpoint if unset. +NEXT_PUBLIC_MAINNET_RPC=https://rpc.sentrixchain.com +NEXT_PUBLIC_MAINNET_CHAIN_ID=7119 diff --git a/apps/validators-portal/.gitignore b/apps/validators-portal/.gitignore new file mode 100644 index 0000000..9125795 --- /dev/null +++ b/apps/validators-portal/.gitignore @@ -0,0 +1,27 @@ +# dependencies +/node_modules + +# next.js +/.next/ +/out/ + +# production +/build + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env +.env*.local +.env + +# typescript +*.tsbuildinfo +next-env.d.ts diff --git a/apps/validators-portal/README.md b/apps/validators-portal/README.md new file mode 100644 index 0000000..0d679c4 --- /dev/null +++ b/apps/validators-portal/README.md @@ -0,0 +1,61 @@ +# @sentriscloud/landing + +Company landing page for **sentriscloud.com**. + +## Stack + +- Next.js 15 (App Router) + React 19 + TypeScript +- Tailwind CSS 4 (CSS-first config in `app/globals.css`) +- next-themes (dark default) +- framer-motion (scroll reveals) +- viem (live mainnet stats) +- lucide-react (icons) + +## Editing copy + +All user-facing copy lives in `content/`: + +| File | Purpose | +| --- | --- | +| `content/site.ts` | Site name, tagline, description, social URLs, brand asset URLs | +| `content/products.ts` | Product cards: name, tagline, description, status, href | +| `content/values.ts` | "Why SentrisCloud" value props | +| `content/nav.ts` | Top nav and footer link grids | + +Editing copy never touches JSX — change the data, the section re-renders automatically. + +## Adding a section + +1. Create `components/sections/.tsx` +2. Export a function component +3. Import in `app/page.tsx` and place it where you want +4. If it has data, put data in `content/.ts` + +## Live stats + +`components/sections/stats.tsx` is a server component that calls `getChainSnapshot()` (in `lib/chain.ts`) which queries the mainnet RPC. The whole page is revalidated every 60s (`export const revalidate = 60` in `app/page.tsx`) — keeps RPC traffic predictable. + +Override the RPC by setting `NEXT_PUBLIC_MAINNET_RPC` in `.env.local`. + +## Run + +From the monorepo root: + +```bash +pnpm -F @sentriscloud/landing dev +# → http://localhost:3000 +``` + +Build: + +```bash +pnpm -F @sentriscloud/landing build +``` + +## Deploy + +Vercel-ready: `Root Directory` = `apps/landing`, `Install Command` = `pnpm install`, `Build Command` = `pnpm build`. Set `NEXT_PUBLIC_MAINNET_RPC` if not using the default. + +## Design system + +Aesthetic follows `sentris-design`: editorial luxury (Playfair Display + Sora + IBM Plex Mono, emerald-on-canvas dark), no crypto-neon, no glassmorphism. Tokens defined in `app/globals.css` under `@theme`. diff --git a/apps/validators-portal/app/globals.css b/apps/validators-portal/app/globals.css new file mode 100644 index 0000000..0800b65 --- /dev/null +++ b/apps/validators-portal/app/globals.css @@ -0,0 +1,278 @@ +@import "tailwindcss"; + +/* CSS custom properties — defined outside @theme so Tailwind 4 doesn't + strip them during compile (var() indirection inside @theme is unreliable). */ +:root { + /* Brand family — emerald shared with Sentrix Labs */ + --color-emerald-50: #ecfdf5; + --color-emerald-100: #d1fae5; + --color-emerald-300: #6ee7b7; + --color-emerald-400: #34d399; + --color-emerald-500: #10b981; + --color-emerald-600: #059669; + --color-emerald-700: #047857; + --color-emerald-900: #064e3b; + + --color-bronze: #8a5a11; + --color-gold: #dbc17f; + + --color-canvas: #0a0a0c; + --color-canvas-2: #111114; + --color-ink: #f5f5f4; + --color-ink-2: #d6d3d1; + --color-ink-3: #a8a29e; + --color-ink-4: #78716c; + --color-line: #27272a; + --color-line-2: #1c1c1f; + + --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); + --ease-in-out-quart: cubic-bezier(0.76, 0, 0.24, 1); + + color-scheme: dark; +} + +@layer base { + html { + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + text-rendering: optimizeLegibility; + scroll-behavior: smooth; + font-synthesis: none; + } + + body { + background: var(--color-canvas); + color: var(--color-ink); + font-family: var(--font-sora), ui-sans-serif, system-ui, sans-serif; + font-feature-settings: "ss01", "cv11", "kern", "liga"; + font-optical-sizing: auto; + font-variant-ligatures: common-ligatures contextual; + overflow-x: hidden; + } + + ::selection { + background: var(--color-emerald-500); + color: var(--color-canvas); + } + + ::-webkit-scrollbar { width: 10px; height: 10px; } + ::-webkit-scrollbar-track { background: var(--color-canvas); } + ::-webkit-scrollbar-thumb { + background: var(--color-line); + border: 2px solid var(--color-canvas); + border-radius: 5px; + } + + @media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + } +} + +@layer components { + /* Editorial display heading — Fraunces, tuned for tech-editorial. + opsz=144 (display optical size, tighter terminals). + SOFT=50 (slightly humanized — less luxury-bridal, more modern). + No discretionary ligatures — those swashes read as "alay" in a + tech context; we want refined, not decorative. */ + .display { + font-family: var(--font-display), ui-serif, Georgia, "Times New Roman", serif; + font-weight: 500; + font-variation-settings: "opsz" 36; + letter-spacing: -0.025em; + line-height: 0.95; + font-feature-settings: "lnum", "kern", "liga", "calt"; + font-variant-ligatures: common-ligatures contextual; + font-optical-sizing: auto; + text-wrap: balance; + } + + /* Accent variant — same upright serif as .display, slightly heavier + so emerald color + weight bump carry the emphasis instead of italic + (italic serif reads "alay" / calligraphic in a tech context). */ + .display-italic { + font-family: var(--font-display), ui-serif, Georgia, "Times New Roman", serif; + font-style: normal; + font-weight: 600; + font-variation-settings: "opsz" 36; + letter-spacing: -0.026em; + line-height: 0.95; + font-feature-settings: "kern", "liga", "calt"; + font-variant-ligatures: common-ligatures contextual; + font-optical-sizing: auto; + } + + /* Heavy emphasis — Fraunces 700, still optical-display sized. + 800 was too bridal; 700 keeps presence without theatrics. */ + .display-heavy { + font-family: var(--font-display), ui-serif, Georgia, "Times New Roman", serif; + font-weight: 700; + font-variation-settings: "opsz" 36; + letter-spacing: -0.028em; + line-height: 0.92; + font-feature-settings: "lnum", "kern", "liga", "calt"; + font-variant-ligatures: common-ligatures contextual; + font-optical-sizing: auto; + text-wrap: balance; + } + + /* Mono — for code, data, eyebrows. Slashed zero + tabular figures so + numbers don't shimmy when they update live. */ + .mono { + font-family: var(--font-plex-mono), ui-monospace, "SF Mono", "JetBrains Mono", Menlo, monospace; + font-feature-settings: "tnum", "zero", "ss01", "calt"; + font-variant-numeric: tabular-nums slashed-zero; + } + + /* Section number tag — "01 / Hero", editorial cue */ + .section-number { + font-family: var(--font-plex-mono), ui-monospace, monospace; + font-size: 0.6875rem; + font-weight: 500; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--color-ink-4); + } + + .eyebrow { + font-family: var(--font-plex-mono), ui-monospace, monospace; + font-size: 0.6875rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.18em; + color: var(--color-emerald-500); + display: inline-flex; + align-items: center; + gap: 0.5rem; + } + + .eyebrow::before { + content: ""; + display: block; + width: 1.5rem; + height: 1px; + background: currentColor; + opacity: 0.5; + } + + .container-page { + width: 100%; + max-width: 1280px; + margin-inline: auto; + padding-inline: 1.5rem; + } + + @media (min-width: 768px) { + .container-page { padding-inline: 2.5rem; } + } + + /* Pull rule — full-width hairline divider */ + .rule { + border: 0; + border-top: 1px solid var(--color-line); + margin: 0; + } + + /* Subtle grain texture overlay */ + .grain::after { + content: ""; + position: fixed; + inset: 0; + pointer-events: none; + z-index: 1; + opacity: 0.025; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='200' height='200'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); + mix-blend-mode: overlay; + } + + /* Drop-cap (used in the manifesto-style sections) */ + .dropcap::first-letter { + font-family: var(--font-display), ui-serif, serif; + font-size: 4.5em; + line-height: 0.85; + float: left; + margin: 0.05em 0.08em -0.1em 0; + color: var(--color-emerald-500); + font-weight: 500; + } + + /* Corner-lines — signature SentrisCloud card treatment. Four small gold + accents flick to visible on hover, framing the content like editorial + bracketing. */ + .corner-lines { + position: relative; + } + + .corner-lines::before, + .corner-lines::after, + .corner-lines > .cl-bl, + .corner-lines > .cl-br { + content: ""; + position: absolute; + width: 18px; + height: 18px; + border: 1px solid var(--color-gold); + opacity: 0; + transition: opacity 350ms var(--ease-out-expo); + pointer-events: none; + } + + .corner-lines::before { + top: 0; left: 0; + border-right: none; border-bottom: none; + } + + .corner-lines::after { + top: 0; right: 0; + border-left: none; border-bottom: none; + } + + .corner-lines > .cl-bl { + bottom: 0; left: 0; + border-right: none; border-top: none; + } + + .corner-lines > .cl-br { + bottom: 0; right: 0; + border-left: none; border-top: none; + } + + .corner-lines:hover::before, + .corner-lines:hover::after, + .corner-lines:hover > .cl-bl, + .corner-lines:hover > .cl-br { + opacity: 0.85; + } + + /* Oversized backdrop numeral — used behind hero for editorial drama. + Upright (italic numerals get gimmicky); use Newsreader at low + weight so it reads as a quiet structural accent. */ + .backdrop-numeral { + font-family: var(--font-display), ui-serif, serif; + font-style: normal; + font-weight: 400; + font-variation-settings: "opsz" 36; + color: var(--color-canvas-2); + line-height: 0.8; + letter-spacing: -0.05em; + user-select: none; + pointer-events: none; + } + + /* Animated underline link */ + .link-underline { + background-image: linear-gradient(currentColor, currentColor); + background-size: 0 1px; + background-position: 0 100%; + background-repeat: no-repeat; + transition: background-size 300ms var(--ease-out-expo); + } + + .link-underline:hover { + background-size: 100% 1px; + } +} diff --git a/apps/validators-portal/app/icon.png b/apps/validators-portal/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..5a3137a95822b55aea70090d95ddfff0524eb1ab GIT binary patch literal 12817 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 + + + + + + + +
+ ), + { ...size }, + ); +} diff --git a/apps/validators-portal/app/layout.tsx b/apps/validators-portal/app/layout.tsx new file mode 100644 index 0000000..cd93578 --- /dev/null +++ b/apps/validators-portal/app/layout.tsx @@ -0,0 +1,83 @@ +import type { Metadata, Viewport } from "next"; + +import { site } from "@/content/site"; +import { display, sora, plexMono } from "@/lib/fonts"; +import { cn } from "@/lib/utils"; +import { ThemeProvider } from "@/components/theme-provider"; + +import "./globals.css"; + +export const metadata: Metadata = { + metadataBase: new URL(site.url), + title: { + default: `${site.name} — ${site.tagline}`, + template: `%s · ${site.name}`, + }, + description: site.description, + keywords: [ + "Sentrix Chain", + "SentrisCloud", + "Layer 1 blockchain", + "block explorer", + "wallet", + "faucet", + "DEX", + "SRX", + ], + authors: [{ name: site.name }], + creator: site.name, + openGraph: { + type: "website", + url: site.url, + siteName: site.name, + title: `${site.name} — ${site.tagline}`, + description: site.description, + images: [ + { + url: "/opengraph-image", + width: 1200, + height: 630, + alt: `${site.name} — ${site.tagline}`, + }, + ], + }, + twitter: { + card: "summary_large_image", + title: `${site.name} — ${site.tagline}`, + description: site.description, + images: ["/opengraph-image"], + }, + robots: { + index: true, + follow: true, + googleBot: { index: true, follow: true, "max-image-preview": "large" }, + }, +}; + +export const viewport: Viewport = { + themeColor: [ + { media: "(prefers-color-scheme: dark)", color: "#0a0a0c" }, + { media: "(prefers-color-scheme: light)", color: "#ffffff" }, + ], + width: "device-width", + initialScale: 1, +}; + +export default function RootLayout({ children }: { children: React.ReactNode }) { + return ( + + + +
{children}
+
+ + + ); +} diff --git a/apps/validators-portal/app/opengraph-image.tsx b/apps/validators-portal/app/opengraph-image.tsx new file mode 100644 index 0000000..3fb15db --- /dev/null +++ b/apps/validators-portal/app/opengraph-image.tsx @@ -0,0 +1,97 @@ +import { ImageResponse } from "next/og"; +import { site } from "@/content/site"; + +export const alt = `${site.name} — ${site.tagline}`; +export const size = { width: 1200, height: 630 }; +export const contentType = "image/png"; + +export default function OpengraphImage() { + return new ImageResponse( + ( +
+ {/* Emerald glow accent */} +
+ +
+ + + + + + + + SentrisCloud +
+ +
+ + ── Products built on Sentrix Chain + +

+ The user-facing layer of the Sentrix ecosystem. +

+
+ +
+ SentrixScan · Wallet · Faucet · CoinBlast + sentriscloud.com +
+
+ ), + { ...size }, + ); +} diff --git a/apps/validators-portal/app/page.tsx b/apps/validators-portal/app/page.tsx new file mode 100644 index 0000000..1982900 --- /dev/null +++ b/apps/validators-portal/app/page.tsx @@ -0,0 +1,134 @@ +import Link from "next/link"; +import { ArrowUpRight } from "lucide-react"; + +import { site } from "@/content/site"; + +export const revalidate = 3600; + +export default function HomePage() { + return ( +
+
+ val +
+ +
+
+ + +
+

+ Coming soon · Beta in {new Date().getFullYear()} +

+

+ The Sentrix +
+ validator{" "} + + directory. + +

+ +
+
+

+ Today Sentrix validators show up as 40-character hex + addresses with no moniker layer — fine for a tight initial + set, awkward for a real delegator audience. +

+

+ This portal will fix that: validators register a moniker, + publish a pitch, and delegators pick + delegate with one + wallet click. Read the raw list on Scan in the meantime. +

+
+ + +
+ +
+ + Browse validators on Scan + + + + Join waitlist + + + + Register a validator + + +
+
+
+ +
+
+

© Sentrix Labs · permissionless validator set

+
+ + sentrixchain.com + + + docs + + + scan + + + {site.email.contact} + +
+
+
+
+
+ ); +} diff --git a/apps/validators-portal/app/robots.ts b/apps/validators-portal/app/robots.ts new file mode 100644 index 0000000..aa4c5ed --- /dev/null +++ b/apps/validators-portal/app/robots.ts @@ -0,0 +1,10 @@ +import type { MetadataRoute } from "next"; +import { site } from "@/content/site"; + +export default function robots(): MetadataRoute.Robots { + return { + rules: [{ userAgent: "*", allow: "/" }], + sitemap: `${site.url}/sitemap.xml`, + host: site.url, + }; +} diff --git a/apps/validators-portal/app/sitemap.ts b/apps/validators-portal/app/sitemap.ts new file mode 100644 index 0000000..6d0d863 --- /dev/null +++ b/apps/validators-portal/app/sitemap.ts @@ -0,0 +1,14 @@ +import type { MetadataRoute } from "next"; +import { site } from "@/content/site"; + +export default function sitemap(): MetadataRoute.Sitemap { + const lastModified = new Date(); + return [ + { + url: site.url, + lastModified, + changeFrequency: "weekly", + priority: 1, + }, + ]; +} diff --git a/apps/validators-portal/components/scroll-reveal.tsx b/apps/validators-portal/components/scroll-reveal.tsx new file mode 100644 index 0000000..7907e01 --- /dev/null +++ b/apps/validators-portal/components/scroll-reveal.tsx @@ -0,0 +1,29 @@ +"use client"; + +import { motion, type HTMLMotionProps } from "framer-motion"; +import type { ReactNode } from "react"; + +type Props = HTMLMotionProps<"div"> & { + children: ReactNode; + delay?: number; +}; + +/** + * Subtle in-view reveal. Starts at opacity 0.6 (still readable) and animates + * to 1 — so SSR / no-JS content remains visible, and the generic + * marketing-spam reveal feel is dialled down. Don't use on hero / + * above-the-fold content. + */ +export function ScrollReveal({ children, delay = 0, ...rest }: Props) { + return ( + + {children} + + ); +} diff --git a/apps/validators-portal/components/theme-provider.tsx b/apps/validators-portal/components/theme-provider.tsx new file mode 100644 index 0000000..e478fc4 --- /dev/null +++ b/apps/validators-portal/components/theme-provider.tsx @@ -0,0 +1,7 @@ +"use client"; + +import { ThemeProvider as NextThemesProvider, type ThemeProviderProps } from "next-themes"; + +export function ThemeProvider({ children, ...props }: ThemeProviderProps) { + return {children}; +} diff --git a/apps/validators-portal/components/theme-toggle.tsx b/apps/validators-portal/components/theme-toggle.tsx new file mode 100644 index 0000000..54d922b --- /dev/null +++ b/apps/validators-portal/components/theme-toggle.tsx @@ -0,0 +1,39 @@ +"use client"; + +import { Moon, Sun } from "lucide-react"; +import { useTheme } from "next-themes"; +import { useSyncExternalStore } from "react"; + +// useSyncExternalStore-based mount detection — equivalent to the classic +// useState+useEffect pattern but lint-clean under React 19's +// react-hooks/set-state-in-effect rule. Server snapshot is false, client +// resolves true on subscribe — same hydration-safe behavior. +const subscribeMount = () => () => {}; +const getMountSnapshot = () => true; +const getMountServerSnapshot = () => false; + +export function ThemeToggle() { + const { resolvedTheme, setTheme } = useTheme(); + const mounted = useSyncExternalStore( + subscribeMount, + getMountSnapshot, + getMountServerSnapshot, + ); + + if (!mounted) { + return
; + } + + const isDark = resolvedTheme === "dark"; + + return ( + + ); +} diff --git a/apps/validators-portal/components/ui/eyebrow.tsx b/apps/validators-portal/components/ui/eyebrow.tsx new file mode 100644 index 0000000..ed7e9ba --- /dev/null +++ b/apps/validators-portal/components/ui/eyebrow.tsx @@ -0,0 +1,5 @@ +import { cn } from "@/lib/utils"; + +export function Eyebrow({ children, className }: { children: React.ReactNode; className?: string }) { + return {children}; +} diff --git a/apps/validators-portal/components/ui/logo.tsx b/apps/validators-portal/components/ui/logo.tsx new file mode 100644 index 0000000..2641461 --- /dev/null +++ b/apps/validators-portal/components/ui/logo.tsx @@ -0,0 +1,37 @@ +import { cn } from "@/lib/utils"; + +type Props = { + className?: string; + size?: number; + withWordmark?: boolean; +}; + +/** + * SentrisCloud mark — 5-dot quincunx in family emerald. + * Inlined SVG (no jsDelivr fetch) for above-the-fold render. + */ +export function Logo({ className, size = 28, withWordmark = false }: Props) { + return ( + + + + + + + + + {withWordmark ? ( + SentrisCloud + ) : null} + + ); +} diff --git a/apps/validators-portal/components/ui/section-heading.tsx b/apps/validators-portal/components/ui/section-heading.tsx new file mode 100644 index 0000000..32ea168 --- /dev/null +++ b/apps/validators-portal/components/ui/section-heading.tsx @@ -0,0 +1,32 @@ +import { cn } from "@/lib/utils"; +import { Eyebrow } from "./eyebrow"; + +type Props = { + eyebrow?: string; + title: string; + description?: string; + className?: string; + align?: "left" | "center"; +}; + +export function SectionHeading({ eyebrow, title, description, className, align = "left" }: Props) { + return ( +
+ {eyebrow ? {eyebrow} : null} +

+ {title} +

+ {description ? ( +

+ {description} +

+ ) : null} +
+ ); +} diff --git a/apps/validators-portal/content/nav.ts b/apps/validators-portal/content/nav.ts new file mode 100644 index 0000000..be0fb61 --- /dev/null +++ b/apps/validators-portal/content/nav.ts @@ -0,0 +1,33 @@ +export type NavLink = { + label: string; + href: string; + external?: boolean; +}; + +export const primaryNav: NavLink[] = [ + { label: "Products", href: "#products" }, + { label: "Why us", href: "#why" }, + { label: "Developers", href: "#developers" }, + { label: "Chain", href: "https://sentrixchain.com", external: true }, +]; + +export const footerLinks = { + products: [ + { label: "SentrixScan", href: "https://scan.sentrixchain.com", external: true }, + { label: "Solux", href: "https://solux.sentriscloud.com", external: true }, + { label: "Sentrix Faucet", href: "https://faucet.sentrixchain.com", external: true }, + { label: "CoinBlast", href: "https://coinblast.sentriscloud.com", external: true }, + ], + ecosystem: [ + { label: "Sentrix Chain", href: "https://sentrixchain.com", external: true }, + { label: "Sentrix Labs", href: "https://github.com/sentrix-labs", external: true }, + { label: "Brand kit", href: "https://github.com/sentrix-labs/brand-kit", external: true }, + { label: "Validators", href: "https://scan.sentrixchain.com/validators", external: true }, + ], + company: [ + { label: "About", href: "#about" }, + { label: "Contact", href: "mailto:contact@sentriscloud.com" }, + { label: "Security", href: "mailto:security@sentriscloud.com" }, + { label: "GitHub", href: "https://github.com/Sentriscloud", external: true }, + ], +} as const; diff --git a/apps/validators-portal/content/products.ts b/apps/validators-portal/content/products.ts new file mode 100644 index 0000000..706c7f6 --- /dev/null +++ b/apps/validators-portal/content/products.ts @@ -0,0 +1,73 @@ +/** + * Products shown in the products grid. + * Edit copy / add new products here — components re-render automatically. + */ +import type { LucideIcon } from "lucide-react"; +import { Search, Wallet, Droplets, TrendingUp } from "lucide-react"; + +export type ProductStatus = "live" | "beta" | "in-development" | "planned"; + +export type Product = { + slug: string; + name: string; + tagline: string; + description: string; + href: string; + status: ProductStatus; + icon: LucideIcon; + /** External — opens in new tab. */ + external?: boolean; +}; + +export const products = [ + { + slug: "scan", + name: "SentrixScan", + tagline: "Block explorer for Sentrix Chain", + description: + "Browse blocks, transactions, addresses, validators, and SRC-20 tokens. Real-time stats, smart search, mainnet + testnet.", + href: "https://scan.sentrixchain.com", + status: "live", + icon: Search, + external: true, + }, + { + slug: "solux", + name: "Solux", + tagline: "Self-custody wallet for SRX", + description: + "Send, receive, and manage SRX and SRC-20 tokens. Web today, mobile (Flutter) in development. Same brand, same keys, two surfaces.", + href: "https://solux.sentriscloud.com", + status: "in-development", + icon: Wallet, + external: true, + }, + { + slug: "faucet", + name: "Sentrix Faucet", + tagline: "Testnet token tap for builders", + description: + "Request testnet SRX in seconds. Rate-limited, no signup, ready for CI and integration testing.", + href: "https://faucet.sentrixchain.com", + status: "live", + icon: Droplets, + external: true, + }, + { + slug: "coinblast", + name: "CoinBlast", + tagline: "DEX + token launchpad", + description: + "Fair-launch tooling, bonding curves, and an integrated DEX for the Sentrix ecosystem. Coming soon.", + href: "#", + status: "planned", + icon: TrendingUp, + }, +] as const satisfies readonly Product[]; + +export const statusLabels: Record = { + live: "Live", + beta: "Beta", + "in-development": "In development", + planned: "Planned", +}; diff --git a/apps/validators-portal/content/site.ts b/apps/validators-portal/content/site.ts new file mode 100644 index 0000000..720ab47 --- /dev/null +++ b/apps/validators-portal/content/site.ts @@ -0,0 +1,30 @@ +/** + * Site-wide metadata + branding constants. + * Edit here once; pulled by layout, OG image, sitemap, robots. + */ +export const site = { + name: "Sentrix Validators", + tagline: "Validator directory + delegation portal for Sentrix Chain.", + description: + "Browse Sentrix Chain validators by stake, commission, and uptime. Delegate SRX with a wallet click. Read-only directory + delegate flow coming soon.", + url: "https://validators.sentrixchain.com", + email: { + contact: "validators@sentrixchain.com", + security: "security@sentriscloud.com", + }, + social: { + github: "https://github.com/sentrix-labs/sentrix", + twitter: "https://x.com/sentriscloud", + telegram: "https://t.me/SentrixChain", + }, + related: { + chain: "https://sentrixchain.com", + docs: "https://docs.sentrixchain.com/operations/validator-guide", + scan: "https://scan.sentrixchain.com/validators", + register: "https://register.sentrixchain.com", + }, + marks: { + logoPng: "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/png-transparent/sentriscloud-512.png", + logoSvg: "https://cdn.jsdelivr.net/gh/sentrix-labs/brand-kit@master/svg/sentriscloud-mark.svg", + }, +} as const; diff --git a/apps/validators-portal/content/values.ts b/apps/validators-portal/content/values.ts new file mode 100644 index 0000000..2008bc1 --- /dev/null +++ b/apps/validators-portal/content/values.ts @@ -0,0 +1,32 @@ +/** + * "Why SentrisCloud" value props. Edit copy here. + */ +import type { LucideIcon } from "lucide-react"; +import { Layers, ShieldCheck, Users } from "lucide-react"; + +export type Value = { + title: string; + description: string; + icon: LucideIcon; +}; + +export const values: Value[] = [ + { + title: "One ecosystem, four surfaces", + description: + "Every product shares chain primitives — wallet, explorer, faucet, exchange — so the developer story stays coherent and the user experience stays familiar across them.", + icon: Layers, + }, + { + title: "Built on Sentrix Chain", + description: + "Native Layer 1 with sub-second blocks and instant finality. We don't ride someone else's settlement layer — we operate the protocol our products depend on.", + icon: ShieldCheck, + }, + { + title: "Open to validators and builders", + description: + "External validators onboarding in 2026. SDKs, brand assets, and tooling are public. We grow the network, not just the product.", + icon: Users, + }, +]; diff --git a/apps/validators-portal/eslint.config.mjs b/apps/validators-portal/eslint.config.mjs new file mode 100644 index 0000000..4bdf495 --- /dev/null +++ b/apps/validators-portal/eslint.config.mjs @@ -0,0 +1,30 @@ +import { defineConfig, globalIgnores } from "eslint/config"; +import nextVitals from "eslint-config-next/core-web-vitals"; +import nextTs from "eslint-config-next/typescript"; + +const eslintConfig = defineConfig([ + ...nextVitals, + ...nextTs, + globalIgnores([ + ".next/**", + "out/**", + "build/**", + "next-env.d.ts", + ]), + { + rules: { + // TODO: re-enable once the React 19 / react-compiler refactor lands. + // The eslint-plugin-react-hooks v6 (pulled in via eslint-config-next 16) + // ships new compiler-aware rules that flag ~30 violations across the + // monorepo — every one is a real refactor, not a quick fix. + "react-hooks/preserve-manual-memoization": "off", + "react-hooks/purity": "off", + "react-hooks/refs": "off", + "react-hooks/set-state-in-effect": "off", + "react-hooks/static-components": "off", + "react-hooks/use-memo": "off", + }, + }, +]); + +export default eslintConfig; diff --git a/apps/validators-portal/globals.d.ts b/apps/validators-portal/globals.d.ts new file mode 100644 index 0000000..6ec1735 --- /dev/null +++ b/apps/validators-portal/globals.d.ts @@ -0,0 +1,9 @@ +/// +/// + +// CSS side-effect imports (e.g. `import "./globals.css"`) +// Typically declared by the auto-generated next-env.d.ts, but that +// file is gitignored + only emitted after `next build`. CI runs +// `pnpm typecheck` before `next build`, so we mirror those refs +// here to keep TS 6 happy. +declare module "*.css"; diff --git a/apps/validators-portal/lib/chain.ts b/apps/validators-portal/lib/chain.ts new file mode 100644 index 0000000..d41dcbf --- /dev/null +++ b/apps/validators-portal/lib/chain.ts @@ -0,0 +1,54 @@ +import { createPublicClient, http, type Chain } from "viem"; + +const MAINNET_RPC = process.env.NEXT_PUBLIC_MAINNET_RPC ?? "https://rpc.sentrixchain.com"; +const MAINNET_CHAIN_ID = Number(process.env.NEXT_PUBLIC_MAINNET_CHAIN_ID ?? 7119); + +export const sentrixMainnet: Chain = { + id: MAINNET_CHAIN_ID, + name: "Sentrix Chain", + nativeCurrency: { name: "Sentrix", symbol: "SRX", decimals: 18 }, + rpcUrls: { + default: { http: [MAINNET_RPC] }, + }, + blockExplorers: { + default: { name: "Sentrix Scan", url: "https://scan.sentrixchain.com" }, + }, +}; + +export const publicClient = createPublicClient({ + chain: sentrixMainnet, + transport: http(MAINNET_RPC, { timeout: 5_000 }), +}); + +export type ChainSnapshot = { + blockHeight: number; + blockTime: number; + validatorCount: number | null; + status: "live" | "stale"; + fetchedAt: number; +}; + +export async function getChainSnapshot(): Promise { + try { + const [blockNumber, block] = await Promise.all([ + publicClient.getBlockNumber(), + publicClient.getBlock({ blockTag: "latest" }), + ]); + + return { + blockHeight: Number(blockNumber), + blockTime: Number(block.timestamp), + validatorCount: null, + status: "live", + fetchedAt: Date.now(), + }; + } catch { + return { + blockHeight: 0, + blockTime: 0, + validatorCount: null, + status: "stale", + fetchedAt: Date.now(), + }; + } +} diff --git a/apps/validators-portal/lib/fonts.ts b/apps/validators-portal/lib/fonts.ts new file mode 100644 index 0000000..b09a1a9 --- /dev/null +++ b/apps/validators-portal/lib/fonts.ts @@ -0,0 +1,31 @@ +import { Newsreader, Sora, IBM_Plex_Mono } from "next/font/google"; + +/** + * Editorial display — Newsreader. + * News-grade serif with optical sizing (opsz axis). Italic is restrained + * (true italic, not script-y swashes), which is what we want for + * "editorial tech" rather than "luxury bridal magazine". Variable so we + * use one file for the whole weight + opsz + italic range. + */ +export const display = Newsreader({ + subsets: ["latin"], + display: "swap", + variable: "--font-display", + weight: "variable", + style: ["normal", "italic"], + axes: ["opsz"], +}); + +export const sora = Sora({ + subsets: ["latin"], + display: "swap", + variable: "--font-sora", + weight: ["200", "300", "400", "500", "600", "700"], +}); + +export const plexMono = IBM_Plex_Mono({ + subsets: ["latin"], + display: "swap", + variable: "--font-plex-mono", + weight: ["400", "500"], +}); diff --git a/apps/validators-portal/lib/utils.ts b/apps/validators-portal/lib/utils.ts new file mode 100644 index 0000000..f7cfc31 --- /dev/null +++ b/apps/validators-portal/lib/utils.ts @@ -0,0 +1,14 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} + +export function formatNumber(n: number, opts?: Intl.NumberFormatOptions): string { + return new Intl.NumberFormat("en-US", opts).format(n); +} + +export function formatCompact(n: number): string { + return new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 2 }).format(n); +} diff --git a/apps/validators-portal/next.config.ts b/apps/validators-portal/next.config.ts new file mode 100644 index 0000000..a48b24c --- /dev/null +++ b/apps/validators-portal/next.config.ts @@ -0,0 +1,18 @@ +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = { + poweredByHeader: false, + compress: true, + images: { + formats: ["image/avif", "image/webp"], + remotePatterns: [ + { protocol: "https", hostname: "cdn.jsdelivr.net" }, + { protocol: "https", hostname: "raw.githubusercontent.com" }, + ], + }, + experimental: { + optimizePackageImports: ["lucide-react"], + }, +}; + +export default nextConfig; diff --git a/apps/validators-portal/package.json b/apps/validators-portal/package.json new file mode 100644 index 0000000..a530b34 --- /dev/null +++ b/apps/validators-portal/package.json @@ -0,0 +1,34 @@ +{ + "name": "@sentriscloud/validators-portal", + "version": "0.1.0", + "private": true, + "scripts": { + "dev": "next dev --turbopack", + "build": "next build", + "start": "next start", + "lint": "eslint", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "clsx": "^2.1.1", + "framer-motion": "^12.38.0", + "lucide-react": "^1.14.0", + "next": "15.5.18", + "next-themes": "^0.4.6", + "react": "19.1.0", + "react-dom": "19.1.0", + "tailwind-merge": "^3.5.0", + "viem": "^2.48.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3", + "@tailwindcss/postcss": "^4", + "@types/node": "^25", + "@types/react": "^19", + "@types/react-dom": "^19", + "eslint": "^9", + "eslint-config-next": "16.2.6", + "tailwindcss": "^4", + "typescript": "^6" + } +} diff --git a/apps/validators-portal/postcss.config.mjs b/apps/validators-portal/postcss.config.mjs new file mode 100644 index 0000000..c7bcb4b --- /dev/null +++ b/apps/validators-portal/postcss.config.mjs @@ -0,0 +1,5 @@ +const config = { + plugins: ["@tailwindcss/postcss"], +}; + +export default config; diff --git a/apps/validators-portal/tsconfig.json b/apps/validators-portal/tsconfig.json new file mode 100644 index 0000000..616b0b8 --- /dev/null +++ b/apps/validators-portal/tsconfig.json @@ -0,0 +1,34 @@ +{ + "extends": "../../tsconfig.base.json", + "compilerOptions": { + "allowJs": true, + "noEmit": true, + "verbatimModuleSyntax": false, + "noUncheckedIndexedAccess": false, + "lib": [ + "dom", + "dom.iterable", + "esnext" + ], + "plugins": [ + { + "name": "next" + } + ], + "paths": { + "@/*": [ + "./*" + ] + } + }, + "include": [ + "next-env.d.ts", + "**/*.ts", + "**/*.tsx", + ".next/types/**/*.ts", + "globals.d.ts" + ], + "exclude": [ + "node_modules" + ] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 48abcfd..10be245 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -449,6 +449,64 @@ importers: specifier: ^6 version: 6.0.3 + apps/register: + dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 + framer-motion: + specifier: ^12.38.0 + version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + lucide-react: + specifier: ^1.14.0 + version: 1.14.0(react@19.1.0) + next: + specifier: 15.5.18 + version: 15.5.18(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + tailwind-merge: + specifier: ^3.5.0 + version: 3.6.0 + viem: + specifier: ^2.48.0 + version: 2.48.11(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)(zod@4.4.3) + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.5 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.3.0 + '@types/node': + specifier: ^25 + version: 25.7.0 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + eslint: + specifier: ^9 + version: 9.39.4(jiti@2.7.0) + eslint-config-next: + specifier: 16.2.6 + version: 16.2.6(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + tailwindcss: + specifier: ^4 + version: 4.3.0 + typescript: + specifier: ^6 + version: 6.0.3 + apps/scan: dependencies: '@base-ui/react': @@ -601,6 +659,64 @@ importers: specifier: ^6 version: 6.0.3 + apps/validators-portal: + dependencies: + clsx: + specifier: ^2.1.1 + version: 2.1.1 + framer-motion: + specifier: ^12.38.0 + version: 12.38.0(@emotion/is-prop-valid@1.4.0)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + lucide-react: + specifier: ^1.14.0 + version: 1.14.0(react@19.1.0) + next: + specifier: 15.5.18 + version: 15.5.18(@babel/core@7.29.0)(@opentelemetry/api@1.9.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + next-themes: + specifier: ^0.4.6 + version: 0.4.6(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + react: + specifier: 19.1.0 + version: 19.1.0 + react-dom: + specifier: 19.1.0 + version: 19.1.0(react@19.1.0) + tailwind-merge: + specifier: ^3.5.0 + version: 3.6.0 + viem: + specifier: ^2.48.0 + version: 2.48.11(bufferutil@4.1.0)(typescript@6.0.3)(utf-8-validate@5.0.10)(zod@4.4.3) + devDependencies: + '@eslint/eslintrc': + specifier: ^3 + version: 3.3.5 + '@tailwindcss/postcss': + specifier: ^4 + version: 4.3.0 + '@types/node': + specifier: ^25 + version: 25.7.0 + '@types/react': + specifier: ^19 + version: 19.2.14 + '@types/react-dom': + specifier: ^19 + version: 19.2.3(@types/react@19.2.14) + eslint: + specifier: ^9 + version: 9.39.4(jiti@2.7.0) + eslint-config-next: + specifier: 16.2.6 + version: 16.2.6(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + tailwindcss: + specifier: ^4 + version: 4.3.0 + typescript: + specifier: ^6 + version: 6.0.3 + packages/wallet-config: devDependencies: '@privy-io/react-auth':