Session 13: The Slate, Africa geo-restriction, OAuth providers, PropRow + GameCard (1311 tests)
This commit is contained in:
@@ -5,6 +5,10 @@ import { useRouter } from 'next/navigation';
|
||||
import { useAuth } from '@/contexts/AuthContext';
|
||||
import { useParlay } from '@/contexts/ParlayContext';
|
||||
import { GradePill } from '@/components/GradeCard';
|
||||
// Session 13 — The Slate is the new browse-first lead surface. The
|
||||
// existing dashboard sections (Most Parlayed, Recent Reads) stay
|
||||
// below as intelligence layers on top of the raw odds.
|
||||
import Slate from '@/components/Slate';
|
||||
|
||||
type Sport = 'NBA' | 'MLB' | 'WNBA';
|
||||
|
||||
@@ -159,8 +163,15 @@ export default function DashboardPage() {
|
||||
)}
|
||||
</header>
|
||||
|
||||
{/* Sport tabs */}
|
||||
<div role="tablist" aria-label="Sport" style={{ display: 'flex', gap: 4, marginBottom: 32, borderBottom: '1px solid var(--border)' }}>
|
||||
{/* Session 13 — Browse-first slate. Owns its own sport-tab UI,
|
||||
search, and inline grading. Renders ABOVE the existing
|
||||
intelligence sections (Top Graded / Most Parlayed / Recent
|
||||
Reads) which serve as supplementary surfaces. */}
|
||||
<Slate tier={tier} />
|
||||
|
||||
{/* Legacy sport tabs — supplementary, kept for the existing
|
||||
Top Graded / Most Parlayed flows below. */}
|
||||
<div role="tablist" aria-label="Sport" style={{ display: 'flex', gap: 4, marginTop: 40, marginBottom: 32, borderBottom: '1px solid var(--border)' }}>
|
||||
{SPORT_TABS.map((s) => {
|
||||
const active = s === sport;
|
||||
const count = gameCountsBySport[s];
|
||||
|
||||
@@ -14,7 +14,7 @@ import CookieConsent from '@/components/CookieConsent';
|
||||
import SentryInit from '@/components/SentryInit';
|
||||
import { LocaleProvider } from '@/contexts/LocaleContext';
|
||||
import { headers } from 'next/headers';
|
||||
import { LOCALE_HEADER, isLocale, DEFAULT_LOCALE, LOCALE_META } from '@/lib/locales';
|
||||
import { LOCALE_HEADER, COUNTRY_HEADER, isLocale, DEFAULT_LOCALE, LOCALE_META } from '@/lib/locales';
|
||||
import './globals.css';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
@@ -97,6 +97,10 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
const localeHeader = hdrs.get(LOCALE_HEADER);
|
||||
const locale = isLocale(localeHeader) ? localeHeader : DEFAULT_LOCALE;
|
||||
const dir = LOCALE_META[locale].dir;
|
||||
// Session 13 — country from CF-IPCountry (set by middleware).
|
||||
// Empty string when traffic bypasses Cloudflare (local dev, direct
|
||||
// origin hits). The Africa-tier gate degrades closed on empty.
|
||||
const country = hdrs.get(COUNTRY_HEADER) || '';
|
||||
|
||||
return (
|
||||
<html lang={locale} dir={dir} className="dark">
|
||||
@@ -109,7 +113,7 @@ export default async function RootLayout({ children }: { children: React.ReactNo
|
||||
/>
|
||||
</head>
|
||||
<body className="antialiased tex-grain">
|
||||
<LocaleProvider locale={locale}>
|
||||
<LocaleProvider locale={locale} country={country}>
|
||||
<PostHogProvider>
|
||||
<AuthProvider>
|
||||
<ExplainModeProvider>
|
||||
|
||||
@@ -10,7 +10,7 @@ function LoginInner() {
|
||||
const router = useRouter();
|
||||
const search = useSearchParams();
|
||||
const next = search.get('next') || '/dashboard';
|
||||
const { signIn, signInWithGoogle } = useAuth();
|
||||
const { signIn, signInWithProvider } = useAuth();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -31,10 +31,20 @@ function LoginInner() {
|
||||
router.replace(next);
|
||||
};
|
||||
|
||||
const handleGoogle = async () => {
|
||||
// Session 13 — generic OAuth dispatch. Apple + X providers must be
|
||||
// configured in the Supabase dashboard (Apple needs a Service ID +
|
||||
// private key; X needs OAuth 2.0 client creds) before the redirect
|
||||
// succeeds. Unconfigured providers return an inline error string
|
||||
// instead of silently failing.
|
||||
const handleOAuth = async (provider: 'google' | 'apple' | 'twitter') => {
|
||||
setBusy(true);
|
||||
await signInWithGoogle();
|
||||
// Supabase redirects to provider; on return AuthContext picks up the session.
|
||||
setError('');
|
||||
const { error: err } = await signInWithProvider(provider);
|
||||
if (err) {
|
||||
setError(err);
|
||||
setBusy(false);
|
||||
}
|
||||
// On success the page redirects to the provider; no state change here.
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -53,9 +63,17 @@ function LoginInner() {
|
||||
Welcome back. Let's read something.
|
||||
</p>
|
||||
|
||||
<button onClick={handleGoogle} disabled={busy} className="btn-ghost" style={{ width: '100%', marginBottom: 16, padding: 12 }}>
|
||||
Continue with Google
|
||||
</button>
|
||||
<div style={{ display: 'grid', gap: 8, marginBottom: 16 }}>
|
||||
<button onClick={() => handleOAuth('google')} disabled={busy} className="btn-ghost" style={{ width: '100%', padding: 12 }}>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button onClick={() => handleOAuth('apple')} disabled={busy} className="btn-ghost" style={{ width: '100%', padding: 12 }}>
|
||||
Continue with Apple
|
||||
</button>
|
||||
<button onClick={() => handleOAuth('twitter')} disabled={busy} className="btn-ghost" style={{ width: '100%', padding: 12 }}>
|
||||
Continue with X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={dividerStyle}>
|
||||
<span style={dividerLine} />
|
||||
|
||||
@@ -10,7 +10,7 @@ function SignupInner() {
|
||||
const router = useRouter();
|
||||
const search = useSearchParams();
|
||||
const next = search.get('next') || '/dashboard';
|
||||
const { signUp, signInWithGoogle } = useAuth();
|
||||
const { signUp, signInWithProvider } = useAuth();
|
||||
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
@@ -40,9 +40,17 @@ function SignupInner() {
|
||||
setTimeout(() => router.replace(next), 1500);
|
||||
};
|
||||
|
||||
const handleGoogle = async () => {
|
||||
// Session 13 — generic OAuth dispatch. Same provider buttons as
|
||||
// the login page; same graceful-error contract for unconfigured
|
||||
// providers (Apple/X).
|
||||
const handleOAuth = async (provider: 'google' | 'apple' | 'twitter') => {
|
||||
setBusy(true);
|
||||
await signInWithGoogle();
|
||||
setError('');
|
||||
const { error: err } = await signInWithProvider(provider);
|
||||
if (err) {
|
||||
setError(err);
|
||||
setBusy(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (done) {
|
||||
@@ -75,9 +83,17 @@ function SignupInner() {
|
||||
5 free reads every month. Your first read is fully unlocked. No credit card.
|
||||
</p>
|
||||
|
||||
<button onClick={handleGoogle} disabled={busy} className="btn-ghost" style={{ width: '100%', marginBottom: 16, padding: 12 }}>
|
||||
Continue with Google
|
||||
</button>
|
||||
<div style={{ display: 'grid', gap: 8, marginBottom: 16 }}>
|
||||
<button onClick={() => handleOAuth('google')} disabled={busy} className="btn-ghost" style={{ width: '100%', padding: 12 }}>
|
||||
Continue with Google
|
||||
</button>
|
||||
<button onClick={() => handleOAuth('apple')} disabled={busy} className="btn-ghost" style={{ width: '100%', padding: 12 }}>
|
||||
Continue with Apple
|
||||
</button>
|
||||
<button onClick={() => handleOAuth('twitter')} disabled={busy} className="btn-ghost" style={{ width: '100%', padding: 12 }}>
|
||||
Continue with X
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div style={dividerStyle}>
|
||||
<span style={dividerLine} />
|
||||
|
||||
Reference in New Issue
Block a user