diff --git a/apps/web/app/(dashboard)/dashboard/page.tsx b/apps/web/app/(dashboard)/dashboard/page.tsx index e29dfc6..02c8d73 100644 --- a/apps/web/app/(dashboard)/dashboard/page.tsx +++ b/apps/web/app/(dashboard)/dashboard/page.tsx @@ -1,10 +1,10 @@ 'use client'; -import Link from 'next/link'; -import { useEffect, useState } from 'react'; -import { apiFetch } from '@/lib/api'; import { StatusPill } from '@/components/status-pill'; import { Button } from '@/components/ui/button'; +import { apiFetch } from '@/lib/api'; +import Link from 'next/link'; +import { useEffect, useState } from 'react'; interface ServerRow { id: string; @@ -37,19 +37,11 @@ export default function Overview() { return (
-
-
-

Overview

-

- Your MCP servers, calls and recent builds. -

-
- - + New server - +
+

Overview

+

+ Your MCP servers, calls and recent builds. +

@@ -61,7 +53,10 @@ export default function Overview() {

Recent servers

- + View all →
@@ -94,9 +89,15 @@ export default function Overview() { {servers.slice(0, 5).map((s) => ( - + - + {s.name} diff --git a/apps/web/app/(marketing)/layout.tsx b/apps/web/app/(marketing)/layout.tsx index 4f72fc7..528e165 100644 --- a/apps/web/app/(marketing)/layout.tsx +++ b/apps/web/app/(marketing)/layout.tsx @@ -1,4 +1,5 @@ import { Logo } from '@/components/logo'; +import { MarketingAuthButtons } from '@/components/marketing-auth-buttons'; import { MarketingMobileMenu } from '@/components/marketing-mobile-menu'; import Link from 'next/link'; @@ -28,18 +29,7 @@ export default function MarketingLayout({ children }: { children: React.ReactNod
- - Sign in - - - Start building - +
diff --git a/apps/web/app/login/page.tsx b/apps/web/app/login/page.tsx index b984968..ee6a13f 100644 --- a/apps/web/app/login/page.tsx +++ b/apps/web/app/login/page.tsx @@ -12,7 +12,7 @@ const ERROR_COPY: Record = { google_state: 'Google sign-in expired or was interrupted. Please try again.', github_failed: 'GitHub sign-in could not be completed. Please try again.', github_state: 'GitHub sign-in expired or was interrupted. Please try again.', - invalid_phone: 'Enter your number in international format, e.g. +41 79 123 45 67.', + invalid_phone: 'That phone number does not look right. Check the country and number.', rate_limited: 'Too many requests. Wait a few minutes and try again.', sms_request_failed: 'Could not send the SMS. Check the number and try again.', invalid_or_expired_code: 'That code has expired. Request a new one.', @@ -21,6 +21,81 @@ const ERROR_COPY: Record = { sms_verify_failed: 'Could not verify the code. Try again.', }; +// Country dial codes for the phone-login picker. Sorted by name; Switzerland +// is the default (Swiss-built product, Swiss Twilio sender number). +const COUNTRIES: { code: string; name: string; dial: string }[] = [ + { code: 'AR', name: 'Argentina', dial: '+54' }, + { code: 'AU', name: 'Australia', dial: '+61' }, + { code: 'AT', name: 'Austria', dial: '+43' }, + { code: 'BE', name: 'Belgium', dial: '+32' }, + { code: 'BR', name: 'Brazil', dial: '+55' }, + { code: 'BG', name: 'Bulgaria', dial: '+359' }, + { code: 'CA', name: 'Canada', dial: '+1' }, + { code: 'CL', name: 'Chile', dial: '+56' }, + { code: 'CN', name: 'China', dial: '+86' }, + { code: 'CO', name: 'Colombia', dial: '+57' }, + { code: 'HR', name: 'Croatia', dial: '+385' }, + { code: 'CZ', name: 'Czechia', dial: '+420' }, + { code: 'DK', name: 'Denmark', dial: '+45' }, + { code: 'EG', name: 'Egypt', dial: '+20' }, + { code: 'EE', name: 'Estonia', dial: '+372' }, + { code: 'FI', name: 'Finland', dial: '+358' }, + { code: 'FR', name: 'France', dial: '+33' }, + { code: 'DE', name: 'Germany', dial: '+49' }, + { code: 'GR', name: 'Greece', dial: '+30' }, + { code: 'HK', name: 'Hong Kong', dial: '+852' }, + { code: 'HU', name: 'Hungary', dial: '+36' }, + { code: 'IS', name: 'Iceland', dial: '+354' }, + { code: 'IN', name: 'India', dial: '+91' }, + { code: 'ID', name: 'Indonesia', dial: '+62' }, + { code: 'IE', name: 'Ireland', dial: '+353' }, + { code: 'IL', name: 'Israel', dial: '+972' }, + { code: 'IT', name: 'Italy', dial: '+39' }, + { code: 'JP', name: 'Japan', dial: '+81' }, + { code: 'KE', name: 'Kenya', dial: '+254' }, + { code: 'LV', name: 'Latvia', dial: '+371' }, + { code: 'LI', name: 'Liechtenstein', dial: '+423' }, + { code: 'LT', name: 'Lithuania', dial: '+370' }, + { code: 'LU', name: 'Luxembourg', dial: '+352' }, + { code: 'MY', name: 'Malaysia', dial: '+60' }, + { code: 'MX', name: 'Mexico', dial: '+52' }, + { code: 'NL', name: 'Netherlands', dial: '+31' }, + { code: 'NZ', name: 'New Zealand', dial: '+64' }, + { code: 'NG', name: 'Nigeria', dial: '+234' }, + { code: 'NO', name: 'Norway', dial: '+47' }, + { code: 'PH', name: 'Philippines', dial: '+63' }, + { code: 'PL', name: 'Poland', dial: '+48' }, + { code: 'PT', name: 'Portugal', dial: '+351' }, + { code: 'RO', name: 'Romania', dial: '+40' }, + { code: 'SA', name: 'Saudi Arabia', dial: '+966' }, + { code: 'RS', name: 'Serbia', dial: '+381' }, + { code: 'SG', name: 'Singapore', dial: '+65' }, + { code: 'SK', name: 'Slovakia', dial: '+421' }, + { code: 'SI', name: 'Slovenia', dial: '+386' }, + { code: 'ZA', name: 'South Africa', dial: '+27' }, + { code: 'KR', name: 'South Korea', dial: '+82' }, + { code: 'ES', name: 'Spain', dial: '+34' }, + { code: 'SE', name: 'Sweden', dial: '+46' }, + { code: 'CH', name: 'Switzerland', dial: '+41' }, + { code: 'TH', name: 'Thailand', dial: '+66' }, + { code: 'TR', name: 'Turkey', dial: '+90' }, + { code: 'UA', name: 'Ukraine', dial: '+380' }, + { code: 'AE', name: 'United Arab Emirates', dial: '+971' }, + { code: 'GB', name: 'United Kingdom', dial: '+44' }, + { code: 'US', name: 'United States', dial: '+1' }, + { code: 'VN', name: 'Vietnam', dial: '+84' }, +]; + +function dialFor(code: string): string { + return COUNTRIES.find((c) => c.code === code)?.dial ?? '+41'; +} + +/** Combine a dial code and a locally-typed number into strict E.164. */ +function toE164(dial: string, local: string): string { + const digits = local.replace(/\D/g, '').replace(/^0+/, ''); + return dial + digits; +} + function errCode(err: unknown): string { const detail = (err as { detail?: { error?: string } }).detail; return detail?.error ?? (err as Error).message ?? 'unknown'; @@ -36,7 +111,9 @@ export default function LoginPage() { const [emailState, setEmailState] = useState<'idle' | 'sending' | 'sent'>('idle'); // SMS one-time code - const [phone, setPhone] = useState(''); + const [country, setCountry] = useState('CH'); + const [phoneLocal, setPhoneLocal] = useState(''); + const [sentTo, setSentTo] = useState(''); const [code, setCode] = useState(''); const [smsStep, setSmsStep] = useState<'phone' | 'code'>('phone'); const [smsBusy, setSmsBusy] = useState(false); @@ -66,8 +143,13 @@ export default function LoginPage() { e.preventDefault(); setSmsBusy(true); setError(null); + const full = toE164(dialFor(country), phoneLocal); try { - await apiFetch('/v1/auth/sms/request', { method: 'POST', body: JSON.stringify({ phone }) }); + await apiFetch('/v1/auth/sms/request', { + method: 'POST', + body: JSON.stringify({ phone: full }), + }); + setSentTo(full); setSmsStep('code'); } catch (err) { setError(ERROR_COPY[errCode(err)] ?? 'Could not send the SMS.'); @@ -83,7 +165,7 @@ export default function LoginPage() { try { await apiFetch('/v1/auth/sms/verify', { method: 'POST', - body: JSON.stringify({ phone, code }), + body: JSON.stringify({ phone: sentTo, code }), }); window.location.href = '/dashboard'; } catch (err) { @@ -201,16 +283,33 @@ export default function LoginPage() { {method === 'phone' && smsStep === 'phone' && (
- + + +
+
+ setPhone(e.target.value)} - placeholder="+41 79 123 45 67" + autoComplete="tel-national" + value={phoneLocal} + onChange={(e) => setPhoneLocal(e.target.value)} + placeholder="79 123 45 67" />