The OAuth backend (signal source, /connect and /callback routes, token refresh, consent grant) was already complete. This adds the missing UI: a Google Health card in /connect with Connect/Disconnect actions, and broadens the "See my tip →" CTA to appear when any integration is connected (not only Todoist). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
239 lines
7.9 KiB
TypeScript
239 lines
7.9 KiB
TypeScript
'use client';
|
|
|
|
import { useEffect, useState, useCallback } from 'react';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import { getIntegrations, disconnectIntegration, deleteAccount, logout } from '@/lib/api';
|
|
import type { Integration } from '@oo/shared-types';
|
|
import { Suspense } from 'react';
|
|
|
|
function ConnectPageInner() {
|
|
const searchParams = useSearchParams();
|
|
const [integrations, setIntegrations] = useState<Integration[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [disconnecting, setDisconnecting] = useState<string | null>(null);
|
|
const [deleting, setDeleting] = useState(false);
|
|
|
|
const load = useCallback(async () => {
|
|
const { integrations: list } = await getIntegrations();
|
|
setIntegrations(list);
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => { load(); }, [load]);
|
|
|
|
// Show banner if just connected
|
|
const justConnected = searchParams.get('connected');
|
|
|
|
const isConnected = (provider: string) =>
|
|
integrations.some((i) => i.provider === provider && i.status === 'connected');
|
|
|
|
const handleDeleteAccount = async () => {
|
|
if (!confirm('Delete your account? This cannot be undone.')) return;
|
|
setDeleting(true);
|
|
await deleteAccount();
|
|
await logout();
|
|
window.location.href = '/sign-in';
|
|
};
|
|
|
|
const handleDisconnect = async (provider: string) => {
|
|
setDisconnecting(provider);
|
|
await disconnectIntegration(provider);
|
|
await load();
|
|
setDisconnecting(null);
|
|
};
|
|
|
|
if (loading) {
|
|
return (
|
|
<main style={{ minHeight: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
|
<div style={{ color: 'var(--gray)', fontSize: '0.875rem' }}>Loading…</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
const todoistConnected = isConnected('todoist');
|
|
const googleHealthConnected = isConnected('google-health');
|
|
const anyConnected = todoistConnected || googleHealthConnected;
|
|
|
|
return (
|
|
<main style={{ minHeight: '100vh', padding: '4rem 2rem', maxWidth: '480px', margin: '0 auto' }}>
|
|
<h2 style={{ fontSize: '1.5rem', fontWeight: 300, marginBottom: '0.5rem', letterSpacing: '-0.02em' }}>
|
|
Connect your apps
|
|
</h2>
|
|
<p style={{ color: 'var(--gray)', fontSize: '0.875rem', marginBottom: '3rem' }}>
|
|
oO reads what you need, when you need it.
|
|
</p>
|
|
|
|
{justConnected && (
|
|
<div style={{
|
|
background: 'rgba(255,255,255,0.06)',
|
|
borderRadius: '0.5rem',
|
|
padding: '0.75rem 1rem',
|
|
marginBottom: '1.5rem',
|
|
fontSize: '0.875rem',
|
|
color: 'var(--white)',
|
|
}}>
|
|
{justConnected} connected.
|
|
</div>
|
|
)}
|
|
|
|
{/* Todoist card */}
|
|
<div style={{
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: '0.75rem',
|
|
padding: '1.25rem 1.5rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: '1rem',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.875rem' }}>
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" aria-label="Todoist">
|
|
<rect width="24" height="24" rx="6" fill="#DB4035"/>
|
|
<path d="M6 8.5L11 13l7-7" stroke="#fff" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"/>
|
|
</svg>
|
|
<div>
|
|
<div style={{ fontWeight: 500, fontSize: '0.9rem' }}>Todoist</div>
|
|
<div style={{ color: 'var(--gray)', fontSize: '0.75rem', marginTop: '0.1rem' }}>
|
|
{todoistConnected ? 'Connected' : 'Tasks & to-dos'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{todoistConnected ? (
|
|
<button
|
|
onClick={() => handleDisconnect('todoist')}
|
|
disabled={disconnecting === 'todoist'}
|
|
style={{
|
|
background: 'transparent',
|
|
border: '1px solid rgba(255,255,255,0.15)',
|
|
color: 'var(--gray)',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
fontSize: '0.8rem',
|
|
}}
|
|
>
|
|
{disconnecting === 'todoist' ? '…' : 'Disconnect'}
|
|
</button>
|
|
) : (
|
|
<a
|
|
href="/api/integrations/todoist/connect?redirectTo=/connect"
|
|
style={{
|
|
background: 'var(--white)',
|
|
color: 'var(--black)',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
fontSize: '0.8rem',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
Connect
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{/* Google Health card */}
|
|
<div style={{
|
|
border: '1px solid rgba(255,255,255,0.1)',
|
|
borderRadius: '0.75rem',
|
|
padding: '1.25rem 1.5rem',
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'space-between',
|
|
marginBottom: '1rem',
|
|
}}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '0.875rem' }}>
|
|
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" aria-label="Google Health">
|
|
<rect width="24" height="24" rx="6" fill="#EA4335"/>
|
|
<path d="M12 6.5c0-1.1.9-2 2-2s2 .9 2 2-.9 2-2 2-2-.9-2-2z" fill="#fff"/>
|
|
<path d="M8 10.5c0-1.1.9-2 2-2s2 .9 2 2-.9 2-2 2-2-.9-2-2z" fill="#fff" opacity=".7"/>
|
|
<path d="M12 14.5c0 2.2-1.8 4-4 4s-4-1.8-4-4 1.8-4 4-4 4 1.8 4 4z" fill="#fff" opacity=".4"/>
|
|
<path d="M13 13.5c.5-1 1.5-1.7 2.5-1.7 1.7 0 3 1.3 3 3s-1.3 3-3 3c-1 0-1.9-.5-2.5-1.3" stroke="#fff" strokeWidth="1.5" strokeLinecap="round" fill="none"/>
|
|
</svg>
|
|
<div>
|
|
<div style={{ fontWeight: 500, fontSize: '0.9rem' }}>Google Health</div>
|
|
<div style={{ color: 'var(--gray)', fontSize: '0.75rem', marginTop: '0.1rem' }}>
|
|
{googleHealthConnected ? 'Connected' : 'Steps, sleep & activity'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{googleHealthConnected ? (
|
|
<button
|
|
onClick={() => handleDisconnect('google-health')}
|
|
disabled={disconnecting === 'google-health'}
|
|
style={{
|
|
background: 'transparent',
|
|
border: '1px solid rgba(255,255,255,0.15)',
|
|
color: 'var(--gray)',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
fontSize: '0.8rem',
|
|
}}
|
|
>
|
|
{disconnecting === 'google-health' ? '…' : 'Disconnect'}
|
|
</button>
|
|
) : (
|
|
<a
|
|
href="/api/integrations/google-health/connect?redirectTo=/connect"
|
|
style={{
|
|
background: 'var(--white)',
|
|
color: 'var(--black)',
|
|
borderRadius: '0.375rem',
|
|
padding: '0.375rem 0.875rem',
|
|
fontSize: '0.8rem',
|
|
fontWeight: 500,
|
|
}}
|
|
>
|
|
Connect
|
|
</a>
|
|
)}
|
|
</div>
|
|
|
|
{anyConnected && (
|
|
<div style={{ marginTop: '3rem' }}>
|
|
<a
|
|
href="/tip"
|
|
style={{
|
|
display: 'block',
|
|
textAlign: 'center',
|
|
background: 'var(--white)',
|
|
color: 'var(--black)',
|
|
borderRadius: '0.5rem',
|
|
padding: '0.875rem',
|
|
fontWeight: 500,
|
|
fontSize: '0.9rem',
|
|
}}
|
|
>
|
|
See my tip →
|
|
</a>
|
|
</div>
|
|
)}
|
|
|
|
<div style={{ marginTop: '4rem', borderTop: '1px solid rgba(255,255,255,0.06)', paddingTop: '2rem' }}>
|
|
<button
|
|
onClick={handleDeleteAccount}
|
|
disabled={deleting}
|
|
style={{
|
|
background: 'transparent',
|
|
border: 'none',
|
|
color: 'rgba(255,255,255,0.2)',
|
|
fontSize: '0.8rem',
|
|
cursor: 'pointer',
|
|
padding: 0,
|
|
}}
|
|
>
|
|
{deleting ? 'Deleting…' : 'Delete account'}
|
|
</button>
|
|
</div>
|
|
</main>
|
|
);
|
|
}
|
|
|
|
export default function ConnectPage() {
|
|
return (
|
|
<Suspense>
|
|
<ConnectPageInner />
|
|
</Suspense>
|
|
);
|
|
}
|