393 lines
12 KiB
TypeScript
393 lines
12 KiB
TypeScript
'use client';
|
||
|
||
import React, { useEffect, useMemo, useState } from 'react';
|
||
import { getAccessToken_client } from '@/utils/apiHelper_client';
|
||
|
||
/** ===== Types from your API ===== */
|
||
type StoreFromAPI = {
|
||
userid: string;
|
||
store_name: string;
|
||
store_description: string;
|
||
store_url: string;
|
||
store_url_path: string;
|
||
store_last_opened_time: string | null;
|
||
store_last_opened_time_raw: string | null;
|
||
store_logo_url: string;
|
||
};
|
||
|
||
type ApiOk = { code: 'STORE_PRESENT'; message: string; store: StoreFromAPI };
|
||
type ApiMiss = { code: 'USER_NOT_FOUND_OR_NO_STORE'; message: string };
|
||
type ApiResp = ApiOk | ApiMiss | any;
|
||
|
||
/** ===== Helpers ===== */
|
||
const read = async (r: Response) =>
|
||
r.headers.get('content-type')?.includes('application/json') ? r.json() : r.text();
|
||
|
||
function safeDateFormat(input?: string | null) {
|
||
if (!input) return '—';
|
||
const d = new Date(input);
|
||
return isNaN(d.getTime()) ? '—' : d.toLocaleString();
|
||
}
|
||
|
||
/** ===== Page ===== */
|
||
export default function EbayAuthPage() {
|
||
const [status, setStatus] = useState<'loading' | 'connected' | 'disconnected'>('loading');
|
||
const [store, setStore] = useState<StoreFromAPI | null>(null);
|
||
const [toast, setToast] = useState<string>('');
|
||
const [connecting, setConnecting] = useState(false);
|
||
|
||
// Build a return URL back to THIS page (works no matter which route you use)
|
||
const returnUrl = useMemo(() => {
|
||
if (typeof window === 'undefined') return '';
|
||
return `${window.location.origin}${window.location.pathname}`;
|
||
}, []);
|
||
|
||
// On load: check store status with only the userid
|
||
useEffect(() => {
|
||
(async () => {
|
||
try {
|
||
// Optional: keep your access token call if you need it for other things
|
||
await getAccessToken_client().catch(() => null);
|
||
|
||
const userid = sessionStorage.getItem('USERID') || undefined;
|
||
if (!userid) {
|
||
setStatus('disconnected');
|
||
setToast('No user session found. Please sign in and try again.');
|
||
return;
|
||
}
|
||
|
||
const res = await fetch(
|
||
'https://ebay.backend.data4autos.com/api/auth/ebay/store/checkstorestatus',
|
||
{
|
||
method: 'POST',
|
||
headers: { 'Content-Type': 'application/json' },
|
||
body: JSON.stringify({ userid }),
|
||
cache: 'no-store',
|
||
}
|
||
);
|
||
|
||
const data: ApiResp = await read(res);
|
||
|
||
if (!res.ok) {
|
||
console.error('checkstorestatus failed:', res.status, data);
|
||
setStatus('disconnected');
|
||
return;
|
||
}
|
||
|
||
if (data?.code === 'STORE_PRESENT' && data?.store) {
|
||
setStore(data.store);
|
||
setStatus('connected');
|
||
setToast('eBay connected successfully');
|
||
} else {
|
||
setStatus('disconnected');
|
||
}
|
||
} catch (err) {
|
||
console.error('checkstorestatus error:', err);
|
||
setStatus('disconnected');
|
||
}
|
||
})();
|
||
}, []);
|
||
|
||
/** Start OAuth if disconnected */
|
||
const OAUTH_ENDPOINT = 'https://ebay.backend.data4autos.com/api/ebay/oauth/login';
|
||
const startOauth = () => {
|
||
try {
|
||
setConnecting(true);
|
||
const url = `${OAUTH_ENDPOINT}?return_url=${encodeURIComponent(returnUrl)}`;
|
||
window.location.href = url;
|
||
} catch {
|
||
setConnecting(false);
|
||
setToast('Failed to start eBay connection. Please try again.');
|
||
}
|
||
};
|
||
|
||
/** Derive UI fields from API store */
|
||
const { title, subtitle, logoUrl, desc, link, lastOpen } = useMemo(() => {
|
||
if (!store) {
|
||
return {
|
||
title: '',
|
||
subtitle: '',
|
||
logoUrl: '',
|
||
desc: '',
|
||
link: '',
|
||
lastOpen: '—',
|
||
};
|
||
}
|
||
return {
|
||
title: `eBay connected! Store ${store.store_url_path}`,
|
||
subtitle: store.store_name || store.store_url_path,
|
||
logoUrl: store.store_logo_url,
|
||
desc: store.store_description,
|
||
link: store.store_url,
|
||
lastOpen: safeDateFormat(store.store_last_opened_time || store.store_last_opened_time_raw),
|
||
};
|
||
}, [store]);
|
||
|
||
return (
|
||
<div style={styles.page}>
|
||
{status === 'connected' && <Confetti />}
|
||
|
||
<div style={styles.cardWrap}>
|
||
{toast && <div style={styles.toast}>{toast}</div>}
|
||
|
||
{status === 'loading' && (
|
||
<>
|
||
<div style={styles.badge}>
|
||
<span style={styles.badgeEmoji}>⏳</span>
|
||
<span style={styles.badgeText}>Checking your store status…</span>
|
||
</div>
|
||
<h1 style={styles.title}>Verifying eBay connection…</h1>
|
||
</>
|
||
)}
|
||
|
||
{status === 'connected' && (
|
||
<>
|
||
<div style={styles.badge}>
|
||
<span style={styles.badgeEmoji}>✅</span>
|
||
<span style={styles.badgeText}>Connected</span>
|
||
</div>
|
||
|
||
<h1 style={styles.title}>
|
||
{title || 'eBay connected successfully!'} <span>🎉</span>
|
||
</h1>
|
||
|
||
<div style={styles.card}>
|
||
<div style={styles.headerRow}>
|
||
{logoUrl ? (
|
||
<img src={logoUrl} alt={subtitle || 'Store Logo'} style={styles.logo} />
|
||
) : (
|
||
<div style={styles.logoFallback}>{(subtitle || 'S').slice(0, 1)}</div>
|
||
)}
|
||
|
||
<div style={{ minWidth: 0 }}>
|
||
<div style={styles.storeName} title={subtitle}>
|
||
{subtitle}
|
||
</div>
|
||
<div style={styles.storeMeta}>
|
||
Last opened: <strong>{lastOpen}</strong>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{desc ? <p style={styles.desc}>{desc}</p> : null}
|
||
|
||
<div style={styles.actions}>
|
||
{link ? (
|
||
<a href={link} target="_blank" rel="noopener noreferrer" style={styles.primaryBtn}>
|
||
Visit eBay Store
|
||
</a>
|
||
) : null}
|
||
<a href="/dashboard" style={styles.secondaryBtn}>
|
||
Go to Dashboard
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</>
|
||
)}
|
||
|
||
{status === 'disconnected' && (
|
||
<div style={styles.connectWrap}>
|
||
<h1 style={styles.title}>eBay Settings</h1>
|
||
<p style={styles.subtleText}>
|
||
Connect your eBay store to enable product sync, inventory updates, and order flow.
|
||
</p>
|
||
|
||
<button
|
||
onClick={startOauth}
|
||
disabled={connecting}
|
||
style={{
|
||
...styles.buttonBase,
|
||
...(connecting ? styles.btnDisabled : styles.btnPrimary),
|
||
}}
|
||
>
|
||
{connecting ? 'Redirecting to eBay…' : 'Connect your eBay store'}
|
||
</button>
|
||
|
||
<p style={styles.hint}>
|
||
You’ll be redirected to eBay to authorize access, then returned here automatically.
|
||
</p>
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
/** ===== Styles (unchanged from your design) ===== */
|
||
const styles: Record<string, React.CSSProperties> = {
|
||
page: {
|
||
minHeight: '100svh',
|
||
background:
|
||
'radial-gradient(1200px 600px at 10% -10%, rgba(99,102,241,0.25), transparent 50%), radial-gradient(1000px 500px at 110% 10%, rgba(16,185,129,0.25), transparent 50%), linear-gradient(180deg, #0b1020 0%, #0b0f1a 100%)',
|
||
padding: '48px 16px',
|
||
color: 'white',
|
||
display: 'flex',
|
||
alignItems: 'center',
|
||
justifyContent: 'center',
|
||
},
|
||
cardWrap: { width: 'min(920px, 92vw)', textAlign: 'center' },
|
||
toast: {
|
||
marginBottom: 12,
|
||
padding: '10px 12px',
|
||
borderRadius: 8,
|
||
border: '1px solid rgba(34,197,94,0.35)',
|
||
background: 'rgba(34,197,94,0.15)',
|
||
color: '#d1fae5',
|
||
fontSize: 14,
|
||
},
|
||
badge: {
|
||
display: 'inline-flex',
|
||
alignItems: 'center',
|
||
gap: 8,
|
||
background: 'rgba(34,197,94,0.15)',
|
||
border: '1px solid rgba(34,197,94,0.35)',
|
||
padding: '8px 12px',
|
||
borderRadius: 999,
|
||
marginBottom: 12,
|
||
},
|
||
badgeEmoji: { fontSize: 18 },
|
||
badgeText: { fontSize: 14, letterSpacing: 0.4 },
|
||
title: { fontSize: 28, lineHeight: 1.25, margin: '0 0 18px', fontWeight: 700 },
|
||
card: {
|
||
textAlign: 'left',
|
||
background: 'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
|
||
border: '1px solid rgba(255,255,255,0.08)',
|
||
borderRadius: 16,
|
||
boxShadow: '0 20px 60px rgba(0,0,0,0.35)',
|
||
padding: 20,
|
||
backdropFilter: 'blur(6px)',
|
||
},
|
||
headerRow: {
|
||
display: 'grid',
|
||
gridTemplateColumns: '72px 1fr',
|
||
gap: 16,
|
||
alignItems: 'center',
|
||
marginBottom: 12,
|
||
},
|
||
logo: {
|
||
width: 72,
|
||
height: 72,
|
||
borderRadius: 12,
|
||
objectFit: 'cover',
|
||
border: '1px solid rgba(255,255,255,0.15)',
|
||
background: 'rgba(255,255,255,0.06)',
|
||
},
|
||
logoFallback: {
|
||
width: 72,
|
||
height: 72,
|
||
borderRadius: 12,
|
||
display: 'grid',
|
||
placeItems: 'center',
|
||
background: 'rgba(255,255,255,0.06)',
|
||
border: '1px solid rgba(255,255,255,0.15)',
|
||
fontSize: 28,
|
||
fontWeight: 700,
|
||
},
|
||
storeName: {
|
||
fontSize: 22,
|
||
fontWeight: 700,
|
||
marginBottom: 4,
|
||
overflow: 'hidden',
|
||
textOverflow: 'ellipsis',
|
||
whiteSpace: 'nowrap',
|
||
},
|
||
storeMeta: { opacity: 0.8, fontSize: 14 },
|
||
desc: { marginTop: 10, marginBottom: 16, lineHeight: 1.6, opacity: 0.95 },
|
||
actions: { display: 'flex', gap: 12, flexWrap: 'wrap' },
|
||
primaryBtn: {
|
||
background: 'linear-gradient(180deg, #22c55e, #16a34a)',
|
||
color: 'white',
|
||
textDecoration: 'none',
|
||
padding: '10px 14px',
|
||
borderRadius: 10,
|
||
fontWeight: 600,
|
||
border: '1px solid rgba(255,255,255,0.15)',
|
||
},
|
||
secondaryBtn: {
|
||
background: 'transparent',
|
||
color: 'white',
|
||
textDecoration: 'none',
|
||
padding: '10px 14px',
|
||
borderRadius: 10,
|
||
fontWeight: 600,
|
||
border: '1px solid rgba(255,255,255,0.2)',
|
||
},
|
||
hint: { marginTop: 14, opacity: 0.75, fontSize: 14 },
|
||
connectWrap: {
|
||
textAlign: 'left',
|
||
background: 'linear-gradient(180deg, rgba(255,255,255,0.04), rgba(255,255,255,0.02))',
|
||
border: '1px solid rgba(255,255,255,0.08)',
|
||
borderRadius: 16,
|
||
boxShadow: '0 20px 60px rgba(0,0,0,0.35)',
|
||
padding: 20,
|
||
backdropFilter: 'blur(6px)',
|
||
},
|
||
subtleText: { opacity: 0.85, marginBottom: 14 },
|
||
buttonBase: {
|
||
width: '100%',
|
||
padding: '10px 14px',
|
||
borderRadius: 10,
|
||
fontWeight: 700,
|
||
border: '1px solid rgba(255,255,255,0.2)',
|
||
cursor: 'pointer',
|
||
},
|
||
btnPrimary: {
|
||
background: 'linear-gradient(180deg, #2563eb, #1d4ed8)',
|
||
color: 'white',
|
||
},
|
||
btnDisabled: {
|
||
opacity: 0.7,
|
||
color: 'white',
|
||
background: 'linear-gradient(180deg, #2563eb, #1d4ed8)',
|
||
cursor: 'not-allowed',
|
||
},
|
||
};
|
||
|
||
/** ===== Confetti (pure CSS) ===== */
|
||
const Confetti = () => {
|
||
const pieces = Array.from({ length: 24 });
|
||
return (
|
||
<div style={confettiStyles.wrap} aria-hidden>
|
||
<style>{confettiKeyframes}</style>
|
||
{pieces.map((_, i) => (
|
||
<span key={i} style={pieceStyle(i)} />
|
||
))}
|
||
</div>
|
||
);
|
||
};
|
||
|
||
const confettiStyles: Record<string, React.CSSProperties> = {
|
||
wrap: { position: 'fixed', inset: 0, pointerEvents: 'none', overflow: 'hidden' },
|
||
};
|
||
|
||
const confettiKeyframes = `
|
||
@keyframes drop {
|
||
0% { transform: translateY(-10vh) rotate(0deg); opacity: 0; }
|
||
10% { opacity: 1; }
|
||
100% { transform: translateY(110vh) rotate(720deg); opacity: 0; }
|
||
}
|
||
`;
|
||
|
||
function pieceStyle(i: number): React.CSSProperties {
|
||
const left = Math.random() * 100;
|
||
const size = 6 + Math.random() * 8;
|
||
const duration = 2.8 + Math.random() * 1.6;
|
||
const delay = Math.random() * 0.8;
|
||
const borderRadius = Math.random() > 0.5 ? 2 : 999;
|
||
return {
|
||
position: 'absolute',
|
||
top: '-10vh',
|
||
left: `${left}vw`,
|
||
width: size,
|
||
height: size,
|
||
background:
|
||
Math.random() > 0.5
|
||
? 'rgba(99,102,241,0.95)'
|
||
: Math.random() > 0.5
|
||
? 'rgba(16,185,129,0.95)'
|
||
: 'rgba(244,114,182,0.95)',
|
||
borderRadius,
|
||
animation: `drop ${duration}s ease-in ${delay}s both`,
|
||
};
|
||
}
|