motorstate-frontend/app/(defaults)/ebay-settings/EbaySettingsClient copy 2.tsx
2025-12-26 13:12:37 +00:00

393 lines
12 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

'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}>
Youll 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`,
};
}