diff --git a/src/app/(auth)/login/LoginContent.tsx b/src/app/(auth)/login/LoginContent.tsx index e68ace4..9aa0139 100644 --- a/src/app/(auth)/login/LoginContent.tsx +++ b/src/app/(auth)/login/LoginContent.tsx @@ -78,12 +78,9 @@ export default function LoginContent({ const googleAuthUrl = `${accountServiceBaseUrl}/api/auth/oauth/login/google`; useEffect(() => { - const publicToken = searchParams.get("public_token"); - const userId = searchParams.get("userId"); - const email = searchParams.get("email"); - const role = searchParams.get("role"); + const exchangeCode = searchParams.get("exchange_code"); - if (!publicToken || !userId || !email) { + if (!exchangeCode) { return; } @@ -101,10 +98,7 @@ export default function LoginContent({ "Content-Type": "application/json", }, body: JSON.stringify({ - publicToken, - userId, - email, - role: role || "user", + exchangeCode, }), }); diff --git a/src/app/AppProviders.tsx b/src/app/AppProviders.tsx index f09ced8..a5c1d70 100644 --- a/src/app/AppProviders.tsx +++ b/src/app/AppProviders.tsx @@ -1,6 +1,6 @@ "use client"; -import { useEffect, type ReactNode } from "react"; +import { useEffect, useState, type CSSProperties, type ReactNode } from "react"; import { usePathname } from "next/navigation"; import { ThemeProvider } from "../components/theme"; import { LanguageProvider } from "../i18n/LanguageProvider"; @@ -20,18 +20,42 @@ export function AppProviders({ const { isOpen, isMinimized, close, toggleOpen } = useMoltbotStore(); const applyDefaults = useOpenClawConsoleStore((state) => state.applyDefaults); const pathname = usePathname(); + const [isMobileViewport, setIsMobileViewport] = useState(false); const isOpenClawWorkspace = pathname.startsWith("/xworkmate") || pathname.startsWith("/services/openclaw"); - // Always reserve space if open and not minimized, since we only have "Float/Sidebar" mode now - // and user wants it to NEVER cover the homepage. - const reserveSpace = !isOpenClawWorkspace && isOpen && !isMinimized; + const reserveSpace = + !isOpenClawWorkspace && isOpen && !isMinimized && !isMobileViewport; useEffect(() => { applyDefaults(assistantDefaults); }, [applyDefaults, assistantDefaults]); + useEffect(() => { + if (typeof window === "undefined") { + return; + } + + const mediaQuery = window.matchMedia("(max-width: 1023px)"); + const syncViewport = () => { + setIsMobileViewport(mediaQuery.matches); + }; + + syncViewport(); + mediaQuery.addEventListener("change", syncViewport); + + return () => { + mediaQuery.removeEventListener("change", syncViewport); + }; + }, []); + + useEffect(() => { + if (isMobileViewport && !isOpenClawWorkspace) { + close(); + } + }, [close, isMobileViewport, isOpenClawWorkspace]); + return ( @@ -40,7 +64,7 @@ export function AppProviders({ style={ { "--assistant-reserve-offset": reserveSpace ? "400px" : "0px", - } as React.CSSProperties + } as CSSProperties } className={cn( "flex-1 flex flex-col relative w-full overflow-hidden transition-[padding] duration-300 ease-in-out", diff --git a/src/app/api/admin/sandbox/bind/route.ts b/src/app/api/admin/sandbox/bind/route.ts index 377979f..cdc58da 100644 --- a/src/app/api/admin/sandbox/bind/route.ts +++ b/src/app/api/admin/sandbox/bind/route.ts @@ -3,20 +3,18 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin'] +const WRITE_PERMISSIONS = ['admin.settings.write'] type ErrorPayload = { error: string } -function isAllowedRootEmail(email?: string): boolean { - return email?.trim().toLowerCase() === 'admin@svc.plus' -} - export async function POST(request: NextRequest) { const session = await getAccountSession(request) const user = session.user @@ -25,12 +23,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - if (!isAllowedRootEmail(user.email)) { - return NextResponse.json({ error: 'root_only' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + rootOnly: true, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const headers = new Headers({ diff --git a/src/app/api/admin/sandbox/binding/route.ts b/src/app/api/admin/sandbox/binding/route.ts index 3ab4e5e..d4722c6 100644 --- a/src/app/api/admin/sandbox/binding/route.ts +++ b/src/app/api/admin/sandbox/binding/route.ts @@ -3,20 +3,18 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin'] +const READ_PERMISSIONS = ['admin.settings.read'] type ErrorPayload = { error: string } -function isAllowedRootEmail(email?: string): boolean { - return email?.trim().toLowerCase() === 'admin@svc.plus' -} - export async function GET(request: NextRequest) { const session = await getAccountSession(request) const user = session.user @@ -25,12 +23,13 @@ export async function GET(request: NextRequest) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - if (!isAllowedRootEmail(user.email)) { - return NextResponse.json({ error: 'root_only' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: READ_PERMISSIONS, + rootOnly: true, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } try { diff --git a/src/app/api/admin/users/[userId]/pause/route.ts b/src/app/api/admin/users/[userId]/pause/route.ts index 0cb4e5a..b96ce4f 100644 --- a/src/app/api/admin/users/[userId]/pause/route.ts +++ b/src/app/api/admin/users/[userId]/pause/route.ts @@ -3,11 +3,13 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin', 'operator'] +const WRITE_PERMISSIONS = ['admin.users.pause.write'] type ErrorPayload = { error: string @@ -35,8 +37,12 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const { userId: userIdParam } = await params diff --git a/src/app/api/admin/users/[userId]/renew-uuid/route.ts b/src/app/api/admin/users/[userId]/renew-uuid/route.ts index 8d3f02d..4d52496 100644 --- a/src/app/api/admin/users/[userId]/renew-uuid/route.ts +++ b/src/app/api/admin/users/[userId]/renew-uuid/route.ts @@ -3,11 +3,13 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin', 'operator'] +const WRITE_PERMISSIONS = ['admin.users.renew_uuid.write'] type ErrorPayload = { error: string @@ -35,8 +37,12 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const { userId: userIdParam } = await params diff --git a/src/app/api/admin/users/[userId]/resume/route.ts b/src/app/api/admin/users/[userId]/resume/route.ts index a82fbc9..9296210 100644 --- a/src/app/api/admin/users/[userId]/resume/route.ts +++ b/src/app/api/admin/users/[userId]/resume/route.ts @@ -3,11 +3,13 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin', 'operator'] +const WRITE_PERMISSIONS = ['admin.users.resume.write'] type ErrorPayload = { error: string @@ -35,8 +37,12 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const { userId: userIdParam } = await params diff --git a/src/app/api/admin/users/[userId]/role/route.ts b/src/app/api/admin/users/[userId]/role/route.ts index c3b9122..cbfe9a6 100644 --- a/src/app/api/admin/users/[userId]/role/route.ts +++ b/src/app/api/admin/users/[userId]/role/route.ts @@ -3,11 +3,13 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin'] +const WRITE_PERMISSIONS = ['admin.users.role.write'] type ErrorPayload = { error: string @@ -35,8 +37,12 @@ export async function POST(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const { userId: userIdParam } = await params @@ -78,8 +84,12 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const { userId: userIdParam } = await params diff --git a/src/app/api/admin/users/[userId]/route.ts b/src/app/api/admin/users/[userId]/route.ts index f5c786e..51c8f88 100644 --- a/src/app/api/admin/users/[userId]/route.ts +++ b/src/app/api/admin/users/[userId]/route.ts @@ -3,11 +3,13 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin', 'operator'] +const DELETE_PERMISSIONS = ['admin.users.delete.write'] type ErrorPayload = { error: string @@ -35,8 +37,12 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: DELETE_PERMISSIONS, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const { userId: userIdParam } = await params diff --git a/src/app/api/admin/users/route.ts b/src/app/api/admin/users/route.ts index 1d3aef1..4bed6e0 100644 --- a/src/app/api/admin/users/route.ts +++ b/src/app/api/admin/users/route.ts @@ -3,11 +3,13 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin'] +const WRITE_PERMISSIONS = ['admin.users.role.write'] const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i @@ -48,10 +50,6 @@ function normalizeGroups(value: unknown): string[] | null { return Array.from(new Set(result)) } -function isAllowedRootEmail(email?: string): boolean { - return email?.trim().toLowerCase() === 'admin@svc.plus' -} - export async function POST(request: NextRequest) { const session = await getAccountSession(request) const user = session.user @@ -60,12 +58,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - if (!isAllowedRootEmail(user.email)) { - return NextResponse.json({ error: 'root_only' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + rootOnly: true, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } const body = (await request.json().catch(() => null)) as CreateUserBody | null diff --git a/src/app/api/auth/token/exchange/route.ts b/src/app/api/auth/token/exchange/route.ts index 5ccbc39..7b506f2 100644 --- a/src/app/api/auth/token/exchange/route.ts +++ b/src/app/api/auth/token/exchange/route.ts @@ -7,9 +7,9 @@ const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() export async function POST(request: NextRequest) { try { const payload = await request.json() - const { publicToken, userId, email, role } = payload + const { exchangeCode } = payload - if (!publicToken || !userId || !email) { + if (!exchangeCode || typeof exchangeCode !== 'string') { return NextResponse.json({ success: false, error: 'invalid_request' }, { status: 400 }) } @@ -19,10 +19,7 @@ export async function POST(request: NextRequest) { 'Content-Type': 'application/json', }, body: JSON.stringify({ - public_token: publicToken, - user_id: userId, - email, - roles: role, + exchange_code: exchangeCode, }), cache: 'no-store', }) @@ -33,12 +30,20 @@ export async function POST(request: NextRequest) { } const data = await response.json() - const { access_token, expires_in } = data + const sessionToken = typeof data.token === 'string' && data.token.trim().length > 0 + ? data.token.trim() + : typeof data.access_token === 'string' && data.access_token.trim().length > 0 + ? data.access_token.trim() + : '' + + if (!sessionToken) { + return NextResponse.json({ success: false, error: 'invalid_response' }, { status: 502 }) + } const result = NextResponse.json({ success: true }) - // If backend returns expires_in (seconds), use it; otherwise derive from expiresAt if it exists - const maxAge = typeof expires_in === 'number' ? expires_in : deriveMaxAgeFromExpires(data.expiresAt) - applySessionCookie(result, access_token, maxAge) + const maxAge = + typeof data.expires_in === 'number' ? data.expires_in : deriveMaxAgeFromExpires(data.expiresAt) + applySessionCookie(result, sessionToken, maxAge) return result } catch (error) { diff --git a/src/app/api/sandbox/assume/route.ts b/src/app/api/sandbox/assume/route.ts index a971740..8007147 100644 --- a/src/app/api/sandbox/assume/route.ts +++ b/src/app/api/sandbox/assume/route.ts @@ -3,12 +3,14 @@ export const dynamic = 'force-dynamic' import { NextRequest, NextResponse } from 'next/server' import { applySessionCookie, deriveMaxAgeFromExpires } from '@lib/authGateway' +import { evaluateAccountAdminAccess } from '@server/account/adminAccess' import { getAccountServiceApiBaseUrl } from '@server/serviceConfig' -import { getAccountSession, userHasRole } from '@server/account/session' +import { getAccountSession } from '@server/account/session' import type { AccountUserRole } from '@server/account/session' const ACCOUNT_API_BASE = getAccountServiceApiBaseUrl() const REQUIRED_ROLES: AccountUserRole[] = ['admin'] +const WRITE_PERMISSIONS = ['admin.settings.write'] const ROOT_BACKUP_COOKIE = 'xc_session_root' const SANDBOX_EMAIL = 'sandbox@svc.plus' @@ -17,10 +19,6 @@ type ErrorPayload = { error: string } -function isAllowedRootEmail(email?: string): boolean { - return email?.trim().toLowerCase() === 'admin@svc.plus' -} - function secureCookies(): boolean { if (process.env.NODE_ENV === 'production') { return true @@ -37,12 +35,13 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'unauthenticated' }, { status: 401 }) } - if (!(await userHasRole(user, REQUIRED_ROLES))) { - return NextResponse.json({ error: 'forbidden' }, { status: 403 }) - } - - if (!isAllowedRootEmail(user.email)) { - return NextResponse.json({ error: 'root_only' }, { status: 403 }) + const access = await evaluateAccountAdminAccess(user, { + roles: REQUIRED_ROLES, + permissions: WRITE_PERMISSIONS, + rootOnly: true, + }) + if (!access.allowed) { + return NextResponse.json({ error: access.reason ?? 'forbidden' }, { status: 403 }) } try { @@ -96,4 +95,3 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'upstream_unreachable' }, { status: 502 }) } } - diff --git a/src/app/globals.css b/src/app/globals.css index 577df65..2c63f2a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -1,13 +1,13 @@ -@import 'react-grid-layout/css/styles.css'; -@import 'react-resizable/css/styles.css'; +@import "react-grid-layout/css/styles.css"; +@import "react-resizable/css/styles.css"; @tailwind base; @tailwind components; @tailwind utilities; :root { - --font-geist-sans: 'Geist', sans-serif; - --font-geist-mono: 'Geist Mono', monospace; + --font-geist-sans: "Geist", sans-serif; + --font-geist-mono: "Geist Mono", monospace; --app-shell-nav-offset: 5.5rem; /* Light theme defaults */ @@ -59,7 +59,8 @@ --gradient-primary-from: #3366ff; --gradient-primary-to: #254edb; - --shadow-sm: 0 1px 2px rgba(17, 24, 39, 0.06), 0 1px 3px rgba(17, 24, 39, 0.04); + --shadow-sm: + 0 1px 2px rgba(17, 24, 39, 0.06), 0 1px 3px rgba(17, 24, 39, 0.04); --shadow-md: 0 10px 24px rgba(17, 24, 39, 0.08); --radius-lg: 0.875rem; @@ -91,7 +92,9 @@ body { color: var(--color-text); -webkit-font-smoothing: antialiased; text-rendering: optimizeLegibility; - transition: background-color 150ms ease, color 150ms ease; + transition: + background-color 150ms ease, + color 150ms ease; } button, @@ -145,3 +148,34 @@ button { background: rgba(51, 102, 255, 0.18); } } + +@media (max-width: 1023px) { + .mobile-home-shell { + --color-background: #fbfaf7; + --color-background-muted: #f4f1eb; + --color-surface: #fffdfa; + --color-surface-elevated: rgba(255, 253, 250, 0.96); + --color-surface-translucent: rgba(255, 253, 250, 0.92); + --color-surface-muted: #f1ece5; + --color-surface-hover: #f6f1ea; + --color-surface-border: rgba(15, 23, 42, 0.1); + --color-surface-border-strong: rgba(15, 23, 42, 0.16); + --color-text: #171717; + --color-heading: #0f172a; + --color-text-muted: #525866; + --color-text-subtle: #747b88; + --color-primary: #111827; + --color-primary-hover: #1f2937; + --color-primary-muted: #f1ece5; + --color-primary-border: rgba(15, 23, 42, 0.12); + --color-accent: #2563eb; + --color-accent-muted: #e5edff; + --color-accent-foreground: #1d4ed8; + --gradient-app-from: #fffdf8; + --gradient-app-via: #f8f4ee; + --gradient-app-to: #fbfaf7; + --shadow-sm: + 0 1px 2px rgba(15, 23, 42, 0.05), 0 6px 18px rgba(15, 23, 42, 0.04); + --shadow-md: 0 18px 45px rgba(15, 23, 42, 0.08); + } +} diff --git a/src/app/page.tsx b/src/app/page.tsx index ffecd06..680a0e1 100644 --- a/src/app/page.tsx +++ b/src/app/page.tsx @@ -67,7 +67,7 @@ export default function HomePage() { const { mode, isOpen } = useMoltbotStore(); return ( -
+
-
+
-
+
@@ -104,43 +104,45 @@ export function HeroSection() { const t = translations[language].marketing.home; return ( -
-
-
+
+
+
{t.hero.eyebrow && ( -

+

{t.hero.eyebrow}

)} -

+

{t.hero.title}

-

{t.hero.subtitle}

+

+ {t.hero.subtitle} +

-
+
{user ? ( -
+
{t.signedIn.replace("{{username}}", user.username)}
) : ( - )} - -

{t.trustedBy}

-
+
@@ -149,8 +151,8 @@ export function HeroSection() {
-
-
+
+
{t.heroCards.map((card) => { const Icon = getIcon(card.title, PlusCircle); return ( @@ -174,12 +176,12 @@ export function NextStepsSection() { const t = translations[language].marketing.home; return ( -
-
-

+

+
+

{t.nextSteps.title}

- + {t.nextSteps.badge}
@@ -189,9 +191,9 @@ export function NextStepsSection() { return (
-
+
@@ -284,14 +286,19 @@ export function StatsSection() { ]; return ( -
-
+
+
{displayStats.map((stat, index: number) => ( -
-
+
+
{stat.value}
-

{stat.label}

+

+ {stat.label} +

))}
@@ -345,22 +352,22 @@ export function ShortcutsSection() { })); return ( -
-
+
+
-

+

{t.shortcuts.title}

-

{t.shortcuts.subtitle}

+

{t.shortcuts.subtitle}

-
- - -
@@ -372,12 +379,12 @@ export function ShortcutsSection() { -
+
-
+
{item.title}
@@ -403,7 +410,7 @@ type LatestBlogPost = { function LogoPill({ label }: { label: string }) { return ( - +
{label} diff --git a/src/app/prices/page.tsx b/src/app/prices/page.tsx index b01e982..e137af0 100644 --- a/src/app/prices/page.tsx +++ b/src/app/prices/page.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { useState } from "react"; +import React, { useState, Suspense } from "react"; import Link from "next/link"; import { Check, Shield } from "lucide-react"; @@ -163,7 +163,9 @@ export default function PricesPage() {

- + + + {statusMessage ? (

{statusMessage} diff --git a/src/components/AskAIDialog.tsx b/src/components/AskAIDialog.tsx index 3647a9c..de8dbc0 100644 --- a/src/components/AskAIDialog.tsx +++ b/src/components/AskAIDialog.tsx @@ -50,57 +50,67 @@ export function AskAIDialog({ } return ( -

-
-
-
-

- XWorkmate -

-

- AI Assistant -

+ <> + {open ? ( + + +
-
- - +
+
- -
- -
-
+ ); } diff --git a/src/components/Footer.tsx b/src/components/Footer.tsx index 3805598..4e02433 100644 --- a/src/components/Footer.tsx +++ b/src/components/Footer.tsx @@ -1,6 +1,6 @@ "use client"; import { Github, Linkedin, Moon, Sun, Twitter } from "lucide-react"; -import Link from 'next/link'; +import Link from "next/link"; import { useLanguage } from "../i18n/LanguageProvider"; import { useThemeStore } from "@components/theme"; @@ -36,16 +36,25 @@ export default function Footer() { }; return ( -