TopBar redesign & PostDetailModal improvements (v0.0.87)

TopBar:
- Responsive logo size (adapts to mobile)
- Logo positioned correctly (+5px down, +2px right)
- Brand text "SOCIOWIRE" visible on all sizes
- Tagline "Wired to Life" hidden on mobile
- Smaller action buttons on mobile (w-7 → w-9 responsive)
- Friends button always visible (opens login if not auth)
- Removed duplicate search bar and location button

PostDetailModal:
- Truth score block made more compact
- Comments use correct API fields (username, body)
- Comments auto-open when there are existing comments
- Fixed comment API response handling (ok at end of spread)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-01-20 06:36:37 +00:00
parent 37f879556e
commit 68c1fce064
6 changed files with 1054 additions and 1342 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "sociowire-frontend", "name": "sociowire-frontend",
"private": true, "private": true,
"version": "0.0.75", "version": "0.0.87",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite --host 0.0.0.0", "dev": "vite --host 0.0.0.0",

View File

@ -821,7 +821,7 @@ export async function createPostComment(postId, body) {
body: JSON.stringify({ post_id: postId, body }), body: JSON.stringify({ post_id: postId, body }),
}); });
const data = await res.json().catch(() => ({})); const data = await res.json().catch(() => ({}));
return { ok: res.ok, ...data }; return { ...data, ok: res.ok };
} catch { } catch {
return { ok: false }; return { ok: false };
} }

View File

@ -57,7 +57,7 @@ function parseMergedParams() {
return { qs, hs, get, hash }; return { qs, hs, get, hash };
} }
export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland }) { export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland, onSearch, onMyLocation }) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { const {
loading, loading,
@ -79,45 +79,23 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
} = useAuth(); } = useAuth();
const [showLangMenu, setShowLangMenu] = useState(false); const [showLangMenu, setShowLangMenu] = useState(false);
const [langMenuPos, setLangMenuPos] = useState({ top: 0, right: 0 });
const langBtnRef = useRef(null);
const [showThemeMenu, setShowThemeMenu] = useState(false); const [showThemeMenu, setShowThemeMenu] = useState(false);
const [themeMenuPos, setThemeMenuPos] = useState({ top: 0, right: 0 }); const [showUserMenu, setShowUserMenu] = useState(false);
const langBtnRef = useRef(null);
const themeBtnRef = useRef(null); const themeBtnRef = useRef(null);
const userMenuRef = useRef(null);
const [searchQuery, setSearchQuery] = useState("");
const [searchFocused, setSearchFocused] = useState(false);
const changeLanguage = (lang) => { const changeLanguage = (lang) => {
i18n.changeLanguage(lang); i18n.changeLanguage(lang);
setShowLangMenu(false); setShowLangMenu(false);
}; };
const toggleLangMenu = () => { // Close menus when clicking outside
if (!showLangMenu && langBtnRef.current) {
const rect = langBtnRef.current.getBoundingClientRect();
setLangMenuPos({
top: rect.bottom + 6,
right: window.innerWidth - rect.right,
});
}
setShowLangMenu(!showLangMenu);
setShowThemeMenu(false);
};
const toggleThemeMenu = () => {
if (!showThemeMenu && themeBtnRef.current) {
const rect = themeBtnRef.current.getBoundingClientRect();
setThemeMenuPos({
top: rect.bottom + 6,
right: window.innerWidth - rect.right,
});
}
setShowThemeMenu(!showThemeMenu);
setShowLangMenu(false);
};
// Close lang/theme menu when clicking outside
useEffect(() => { useEffect(() => {
if (!showLangMenu && !showThemeMenu) return; if (!showLangMenu && !showThemeMenu && !showUserMenu) return;
const handleClick = (e) => { const handleClick = (e) => {
if (showLangMenu && langBtnRef.current && !langBtnRef.current.contains(e.target)) { if (showLangMenu && langBtnRef.current && !langBtnRef.current.contains(e.target)) {
setShowLangMenu(false); setShowLangMenu(false);
@ -125,10 +103,13 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
if (showThemeMenu && themeBtnRef.current && !themeBtnRef.current.contains(e.target)) { if (showThemeMenu && themeBtnRef.current && !themeBtnRef.current.contains(e.target)) {
setShowThemeMenu(false); setShowThemeMenu(false);
} }
if (showUserMenu && userMenuRef.current && !userMenuRef.current.contains(e.target)) {
setShowUserMenu(false);
}
}; };
document.addEventListener("click", handleClick); document.addEventListener("click", handleClick);
return () => document.removeEventListener("click", handleClick); return () => document.removeEventListener("click", handleClick);
}, [showLangMenu, showThemeMenu]); }, [showLangMenu, showThemeMenu, showUserMenu]);
const [showAuth, setShowAuth] = useState(false); const [showAuth, setShowAuth] = useState(false);
const [mode, setMode] = useState("login"); const [mode, setMode] = useState("login");
@ -140,14 +121,13 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
const [usernameInfo, setUsernameInfo] = useState(""); const [usernameInfo, setUsernameInfo] = useState("");
const [usernameSaving, setUsernameSaving] = useState(false); const [usernameSaving, setUsernameSaving] = useState(false);
const usernamePromptTimer = useRef(null); const usernamePromptTimer = useRef(null);
const invalidSinceRef = useRef(null);
const [stableUsername, setStableUsername] = useState(""); const [stableUsername, setStableUsername] = useState("");
const displayUsername = (() => { const displayUsername = (() => {
const raw = (stableUsername || username || "user").toString(); const raw = (stableUsername || username || "user").toString();
if (!stableUsername && raw.includes("@")) return "user"; if (!stableUsername && raw.includes("@")) return "user";
if (raw.length <= 9) return raw; if (raw.length <= 12) return raw;
return raw.slice(0, 9) + ".."; return raw.slice(0, 12) + "..";
})(); })();
const [loginUser, setLoginUser] = useState(""); const [loginUser, setLoginUser] = useState("");
@ -229,16 +209,13 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
const loadRequests = async () => { const loadRequests = async () => {
try { try {
const res = await fetchFriendRequests(); const res = await fetchFriendRequests();
// Only count received requests (not sent ones)
const received = (res?.requests || []).filter(r => r?.direction === 'received'); const received = (res?.requests || []).filter(r => r?.direction === 'received');
setPendingRequestsCount(received.length); setPendingRequestsCount(received.length);
} catch (err) { } catch (err) {
console.error('[TopBar] Error fetching friend requests:', err);
setPendingRequestsCount(0); setPendingRequestsCount(0);
} }
}; };
loadRequests(); loadRequests();
// Refresh every 30 seconds
const interval = setInterval(loadRequests, 30000); const interval = setInterval(loadRequests, 30000);
return () => clearInterval(interval); return () => clearInterval(interval);
}, [authenticated]); }, [authenticated]);
@ -303,9 +280,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
useEffect(() => { useEffect(() => {
const handler = (e) => { const handler = (e) => {
const detail = e?.detail || {}; const detail = e?.detail || {};
const msg = const msg = (detail.message || "").toString().trim() || t('auth.pleaseSignIn');
(detail.message || "").toString().trim() ||
t('auth.pleaseSignIn');
setAuthNotice(msg); setAuthNotice(msg);
setMode(detail.mode === "signup" ? "signup" : "login"); setMode(detail.mode === "signup" ? "signup" : "login");
setShowAuth(true); setShowAuth(true);
@ -314,27 +289,19 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
return () => window.removeEventListener("sociowire:auth-required", handler); return () => window.removeEventListener("sociowire:auth-required", handler);
}, []); }, []);
// Check if PWA install should be shown (simple: hide if already PWA) // Check if PWA install should be shown
useEffect(() => { useEffect(() => {
const checkInstall = () => { const checkInstall = () => {
// Hide if already running as PWA
if (isRunningAsPWA()) { if (isRunningAsPWA()) {
setShowInstallBtn(false); setShowInstallBtn(false);
return; return;
} }
// Show if can install
setShowInstallBtn(canInstall()); setShowInstallBtn(canInstall());
}; };
checkInstall(); checkInstall();
const handlePrompt = () => setTimeout(checkInstall, 100);
const handlePrompt = () => {
setTimeout(checkInstall, 100);
};
window.addEventListener('beforeinstallprompt', handlePrompt); window.addEventListener('beforeinstallprompt', handlePrompt);
window.addEventListener('appinstalled', () => setShowInstallBtn(false)); window.addEventListener('appinstalled', () => setShowInstallBtn(false));
return () => { return () => {
window.removeEventListener('beforeinstallprompt', handlePrompt); window.removeEventListener('beforeinstallprompt', handlePrompt);
window.removeEventListener('appinstalled', () => setShowInstallBtn(false)); window.removeEventListener('appinstalled', () => setShowInstallBtn(false));
@ -344,28 +311,11 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
useEffect(() => { useEffect(() => {
try { try {
const { get, qs, hs, hash } = parseMergedParams(); const { get, qs, hs, hash } = parseMergedParams();
const verified = get("verified") || get("validated") || get("email_verified") || get("emailVerified") || get("kc_verified");
const verified =
get("verified") ||
get("validated") ||
get("email_verified") ||
get("emailVerified") ||
get("kc_verified");
const signup = get("signup"); const signup = get("signup");
const notice = get("notice"); const notice = get("notice");
const email = get("email") || get("user_email") || get("mail");
const email = const uname = get("username") || get("user") || get("preferred_username") || get("login");
get("email") ||
get("user_email") ||
get("mail");
const uname =
get("username") ||
get("user") ||
get("preferred_username") ||
get("login");
const last = readLastSignup(); const last = readLastSignup();
if (truthyParam(verified)) { if (truthyParam(verified)) {
@ -373,11 +323,9 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
setSignupInfo("✅ " + t('auth.emailConfirmedMsg') + (who ? " (" + who + ")" : "")); setSignupInfo("✅ " + t('auth.emailConfirmedMsg') + (who ? " (" + who + ")" : ""));
setMode("login"); setMode("login");
setShowAuth(true); setShowAuth(true);
const prefill = (uname || email || last?.username || last?.email || "").trim(); const prefill = (uname || email || last?.username || last?.email || "").trim();
if (prefill) setLoginUser(prefill); if (prefill) setLoginUser(prefill);
} else if (truthyParam(signup)) { } else if (truthyParam(signup)) {
// Open FRESH signup form - clear all old data
try { localStorage.removeItem(LAST_SIGNUP_KEY); } catch {} try { localStorage.removeItem(LAST_SIGNUP_KEY); } catch {}
setSignupInfo(""); setSignupInfo("");
setSignupError(""); setSignupError("");
@ -394,8 +342,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
} }
} }
const touched = const touched = qs.has("verified") || qs.has("validated") || qs.has("email_verified") || qs.has("emailVerified") || qs.has("signup") || qs.has("notice") ||
qs.has("verified") || qs.has("validated") || qs.has("email_verified") || qs.has("emailVerified") || qs.has("signup") || qs.has("notice") ||
hs.has("verified") || hs.has("validated") || hs.has("email_verified") || hs.has("emailVerified") || hs.has("signup") || hs.has("notice"); hs.has("verified") || hs.has("validated") || hs.has("email_verified") || hs.has("emailVerified") || hs.has("signup") || hs.has("notice");
if (touched) { if (touched) {
@ -403,9 +350,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
const clean = window.location.pathname + cleanHash; const clean = window.location.pathname + cleanHash;
window.history.replaceState({}, "", clean); window.history.replaceState({}, "", clean);
} }
} catch (e) { } catch (e) {}
// ignore
}
}, []); }, []);
const handleLoginSubmit = async (e) => { const handleLoginSubmit = async (e) => {
@ -428,35 +373,29 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
try { try {
setSignupLoading(true); setSignupLoading(true);
trackEvent("signup_submit"); trackEvent("signup_submit");
const u = regUser.trim(); const u = regUser.trim();
const em = regEmail.trim(); const em = regEmail.trim();
await registerUser({ username: u, email: em, password: regPass, firstName: firstName.trim(), lastName: lastName.trim() });
await registerUser({ try { localStorage.setItem(LAST_SIGNUP_KEY, JSON.stringify({ username: u, email: em })); } catch {}
username: u,
email: em,
password: regPass,
firstName: firstName.trim(),
lastName: lastName.trim(),
});
try {
localStorage.setItem(LAST_SIGNUP_KEY, JSON.stringify({ username: u, email: em }));
} catch {}
setSignupInfo("✅ " + t('auth.accountCreatedMsg') + " (" + em + ")"); setSignupInfo("✅ " + t('auth.accountCreatedMsg') + " (" + em + ")");
trackEvent("signup_success"); trackEvent("signup_success");
setMode("login"); setMode("login");
setLoginUser(u || em); setLoginUser(u || em);
setLoginPass(""); setLoginPass("");
} catch (err) { } catch (err) {
console.error(err);
setSignupError(err.message || t('auth.registrationError')); setSignupError(err.message || t('auth.registrationError'));
} finally { } finally {
setSignupLoading(false); setSignupLoading(false);
} }
}; };
const handleSearchSubmit = (e) => {
e.preventDefault();
if (searchQuery.trim() && onSearch) {
onSearch(searchQuery.trim());
}
};
const toast = useMemo(() => { const toast = useMemo(() => {
if (lastError) { if (lastError) {
const toastType = needsEmailVerification ? "warn" : "error"; const toastType = needsEmailVerification ? "warn" : "error";
@ -469,10 +408,12 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
return null; return null;
}, [lastError, lastInfo, needsEmailVerification, t]); }, [lastError, lastInfo, needsEmailVerification, t]);
const themeIcon = theme === 'dark' ? 'fa-moon' : theme === 'light' ? 'fa-sun' : 'fa-droplet';
return ( return (
<> <>
{/* Toast area (under topbar) */} {/* Toast area */}
{toast?.message ? ( {toast?.message && (
<div className="sw-toast-wrap"> <div className="sw-toast-wrap">
<AuthToast <AuthToast
type={toast.type} type={toast.type}
@ -484,137 +425,119 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
}} }}
/> />
</div> </div>
) : null}
<header className="topbar">
<div className="brand-stack">
<div className="brand-toprow">
<div className="logo-circle"><img src="/logo.png" alt="SocioWire" /></div>
<div className="title-block">
<div className="main-title">SOCIOWIRE.com</div>
<div className="sub-title">{t('topbar.wiredToLife')}</div>
</div>
</div>
{/* Theme dots */}
<div className="brand-under">
<div className="theme-switch">
{["dark", "blue", "light"].map((themeKey) => (
<button
key={themeKey}
type="button"
className={"theme-dot" + (theme === themeKey ? " theme-dot-active" : "")}
title={t(`theme.${themeKey}`)}
onClick={() => onChangeTheme && onChangeTheme(themeKey)}
>
{t(`theme.${themeKey}`)[0]}
</button>
))}
</div>
</div>
</div>
<div className="topbar-right">
{/* User info */}
<div className="topbar-user-section">
{authenticated ? (
<button
type="button"
className="sw-userchip sw-userchip-btn"
title={username}
onClick={() => onUserIsland && onUserIsland(username)}
>
<div className="sw-avatar">
{avatarUrl ? <img src={toSmallImageUrl(avatarUrl)} alt={username} /> : <span>{initialLetter(username)}</span>}
</div>
<div className="sw-usertext">
<div className="sw-userline">{t('common.online')}</div>
<div className="sw-userhandle">@{displayUsername}</div>
</div>
</button>
) : (
<div className="sw-userchip" title={t('common.guest')}>
<div className="sw-avatar sw-avatar-guest">
<i className="fa-solid fa-user" />
</div>
<div className="sw-usertext">
<div className="sw-userline">{loading ? t('common.loading') : t('common.guest')}</div>
<div className="sw-userhandle">{t('common.anon')}</div>
</div>
</div>
)} )}
<header className="sw-topbar">
{/* LEFT: Logo + Brand */}
<div className="sw-topbar-left">
<div className="sw-logo">
<img src="/logo.png" alt="SocioWire" />
</div>
<div className="sw-brand">
<span className="sw-brand-name">SocioWire</span>
<span className="sw-brand-tagline">{t('topbar.wiredToLife')}</span>
</div>
</div> </div>
{/* Action buttons - separate section */} {/* CENTER: Search Bar */}
<div className="topbar-actions"> <div className={`sw-topbar-center ${searchFocused ? 'is-focused' : ''}`}>
{/* PWA Install button - no dismiss X */} <form onSubmit={handleSearchSubmit} className="sw-search-form">
<i className="fa-solid fa-magnifying-glass sw-search-icon" />
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setSearchFocused(true)}
onBlur={() => setSearchFocused(false)}
placeholder={t('map.searchPlaceholder')}
className="sw-search-input"
/>
{searchQuery && (
<button type="button" className="sw-search-clear" onClick={() => setSearchQuery("")}>
<i className="fa-solid fa-times" />
</button>
)}
</form>
</div>
{/* RIGHT: Actions */}
<div className="sw-topbar-right">
{/* My Location */}
<button
type="button"
className="sw-topbar-btn sw-topbar-btn-location"
onClick={() => onMyLocation && onMyLocation()}
title={t('map.mySpot')}
>
<i className="fa-solid fa-location-crosshairs" />
</button>
{/* Islands */}
<button
type="button"
className="sw-topbar-btn sw-topbar-btn-islands"
onClick={() => onIslandsClick && onIslandsClick()}
title={t('nav.islands')}
>
<i className="fa-solid fa-globe" />
</button>
{/* Install PWA */}
{showInstallBtn && ( {showInstallBtn && (
<button <button
className="btn-action btn-action-install"
type="button" type="button"
className="sw-topbar-btn sw-topbar-btn-install"
onClick={() => promptInstall()} onClick={() => promptInstall()}
title={t('topbar.installApp')} title={t('topbar.installApp')}
> >
<i className="fa-solid fa-download"></i> <i className="fa-solid fa-download" />
</button> </button>
)} )}
{/* Theme selector (mobile dropdown) */} {/* Theme */}
<div className="theme-selector-wrap"> <div className="sw-topbar-dropdown" ref={themeBtnRef}>
<button <button
ref={themeBtnRef}
className={"btn-action btn-action-theme" + (showThemeMenu ? " is-active" : "")}
type="button" type="button"
onClick={toggleThemeMenu} className={`sw-topbar-btn sw-topbar-btn-theme ${showThemeMenu ? 'is-active' : ''}`}
onClick={() => { setShowThemeMenu(!showThemeMenu); setShowLangMenu(false); }}
title={t(`theme.${theme}`)} title={t(`theme.${theme}`)}
> >
<i className="fa-solid fa-palette"></i> <i className={`fa-solid ${themeIcon}`} />
</button> </button>
{showThemeMenu && ( {showThemeMenu && (
<div <div className="sw-dropdown-menu sw-dropdown-theme">
className="theme-dropdown"
style={{ top: themeMenuPos.top, right: themeMenuPos.right }}
>
{THEMES.map((themeKey) => ( {THEMES.map((themeKey) => (
<button <button
key={themeKey} key={themeKey}
type="button" type="button"
className={"theme-option" + (theme === themeKey ? " is-active" : "")} className={`sw-dropdown-item ${theme === themeKey ? 'is-active' : ''}`}
onClick={() => { onClick={() => { onChangeTheme && onChangeTheme(themeKey); setShowThemeMenu(false); }}
onChangeTheme && onChangeTheme(themeKey);
setShowThemeMenu(false);
}}
> >
{t(`theme.${themeKey}`)} <i className={`fa-solid ${themeKey === 'dark' ? 'fa-moon' : themeKey === 'light' ? 'fa-sun' : 'fa-droplet'}`} />
<span>{t(`theme.${themeKey}`)}</span>
</button> </button>
))} ))}
</div> </div>
)} )}
</div> </div>
{/* Language selector */} {/* Language */}
<div className="lang-selector-wrap"> <div className="sw-topbar-dropdown" ref={langBtnRef}>
<button <button
ref={langBtnRef}
className={"btn-action btn-action-lang" + (showLangMenu ? " is-active" : "")}
type="button" type="button"
onClick={toggleLangMenu} className={`sw-topbar-btn sw-topbar-btn-lang ${showLangMenu ? 'is-active' : ''}`}
onClick={() => { setShowLangMenu(!showLangMenu); setShowThemeMenu(false); }}
title={t('language.selectLanguage')} title={t('language.selectLanguage')}
> >
<i className="fa-solid fa-globe"></i> <span className="sw-lang-code">{i18n.language?.toUpperCase().slice(0, 2)}</span>
<span className="btn-label">{i18n.language?.toUpperCase().slice(0, 2)}</span>
</button> </button>
{showLangMenu && ( {showLangMenu && (
<div <div className="sw-dropdown-menu sw-dropdown-lang">
className="lang-dropdown"
style={{ top: langMenuPos.top, right: langMenuPos.right }}
>
{SUPPORTED_LANGS.map((lang) => ( {SUPPORTED_LANGS.map((lang) => (
<button <button
key={lang} key={lang}
type="button" type="button"
className={"lang-option" + (i18n.language?.startsWith(lang) ? " is-active" : "")} className={`sw-dropdown-item ${i18n.language?.startsWith(lang) ? 'is-active' : ''}`}
onClick={() => changeLanguage(lang)} onClick={() => changeLanguage(lang)}
> >
{t(`language.${lang}`)} {t(`language.${lang}`)}
@ -624,44 +547,78 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
)} )}
</div> </div>
{/* Friends button - only when authenticated */} {/* Friends - only when authenticated */}
{authenticated && ( {authenticated && (
<button <button
className={"btn-action btn-action-friends" + (showFriends ? " is-active" : "")}
type="button" type="button"
className={`sw-topbar-btn sw-topbar-btn-friends ${showFriends ? 'is-active' : ''}`}
onClick={() => setShowFriends(!showFriends)} onClick={() => setShowFriends(!showFriends)}
title={t('topbar.friends')} title={t('topbar.friends')}
> >
<i className="fa-solid fa-user-group"></i> <i className="fa-solid fa-user-group" />
{pendingRequestsCount > 0 && ( {pendingRequestsCount > 0 && (
<span className="friends-badge-count">{pendingRequestsCount}</span> <span className="sw-badge">{pendingRequestsCount}</span>
)} )}
</button> </button>
)} )}
{/* Login/Logout button */} {/* User / Login */}
<div className="sw-topbar-dropdown" ref={userMenuRef}>
<button <button
className="btn-action btn-action-auth"
type="button" type="button"
className={`sw-topbar-user ${showUserMenu ? 'is-active' : ''}`}
onClick={() => { onClick={() => {
if (authenticated) { if (authenticated) {
trackEvent("logout_click"); setShowUserMenu(!showUserMenu);
logout();
} else { } else {
openModal("login"); openModal("login");
} }
}} }}
disabled={loading} title={authenticated ? username : t('topbar.login')}
title={authenticated ? t('topbar.logout') : t('topbar.login')}
> >
<i className={authenticated ? "fa-solid fa-right-from-bracket" : "fa-solid fa-right-to-bracket"}></i> <div className="sw-user-avatar">
<span className="btn-label">{authenticated ? t('topbar.logout') : t('topbar.login')}</span> {authenticated && avatarUrl ? (
<img src={toSmallImageUrl(avatarUrl)} alt={username} />
) : authenticated ? (
<span>{initialLetter(username)}</span>
) : (
<i className="fa-solid fa-user" />
)}
</div>
{authenticated && (
<div className="sw-user-info">
<span className="sw-user-name">@{displayUsername}</span>
<span className="sw-user-status">{t('common.online')}</span>
</div>
)}
<i className="fa-solid fa-chevron-down sw-user-chevron" />
</button> </button>
{showUserMenu && authenticated && (
<div className="sw-dropdown-menu sw-dropdown-user">
<button
type="button"
className="sw-dropdown-item"
onClick={() => { setShowUserMenu(false); onUserIsland && onUserIsland(username); }}
>
<i className="fa-solid fa-user" />
<span>{t('map.profileIsland')}</span>
</button>
<button
type="button"
className="sw-dropdown-item sw-dropdown-logout"
onClick={() => { setShowUserMenu(false); trackEvent("logout_click"); logout(); }}
>
<i className="fa-solid fa-right-from-bracket" />
<span>{t('topbar.logout')}</span>
</button>
</div>
)}
</div> </div>
</div> </div>
</header> </header>
{showAuth ? ( {/* Auth Modal */}
{showAuth && (
<div className="auth-modal-backdrop" onClick={closeModal}> <div className="auth-modal-backdrop" onClick={closeModal}>
<div className="auth-modal" onClick={(e) => e.stopPropagation()}> <div className="auth-modal" onClick={(e) => e.stopPropagation()}>
<div className="auth-modal-header"> <div className="auth-modal-header">
@ -679,88 +636,57 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
> >
{t('auth.signup')} {t('auth.signup')}
</button> </button>
<button type="button" className="auth-close" onClick={closeModal}> <button type="button" className="auth-close" onClick={closeModal}></button>
</button>
</div> </div>
{signupInfo ? <div className="auth-notice auth-notice-success">{signupInfo}</div> : null} {signupInfo && <div className="auth-notice auth-notice-success">{signupInfo}</div>}
{authNotice ? <div className="auth-notice auth-notice-warn">{authNotice}</div> : null} {authNotice && <div className="auth-notice auth-notice-warn">{authNotice}</div>}
{mode === "login" && lastError ? ( {mode === "login" && lastError && (
<div className={"auth-notice " + (needsEmailVerification ? "auth-notice-warn" : "auth-notice-error")}> <div className={"auth-notice " + (needsEmailVerification ? "auth-notice-warn" : "auth-notice-error")}>
{lastError} {lastError}
</div> </div>
) : null} )}
{mode === "login" ? ( {mode === "login" ? (
<form className="auth-form" onSubmit={handleLoginSubmit}> <form className="auth-form" onSubmit={handleLoginSubmit}>
<label> <label>
{t('auth.usernameOrEmail')} {t('auth.usernameOrEmail')}
<input <input type="text" value={loginUser} onChange={(e) => setLoginUser(e.target.value)} autoComplete="username" />
type="text"
value={loginUser}
onChange={(e) => setLoginUser(e.target.value)}
autoComplete="username"
/>
</label> </label>
<label> <label>
{t('auth.password')} {t('auth.password')}
<input <input type="password" value={loginPass} onChange={(e) => setLoginPass(e.target.value)} autoComplete="current-password" />
type="password"
value={loginPass}
onChange={(e) => setLoginPass(e.target.value)}
autoComplete="current-password"
/>
</label> </label>
<button <button type="button" className="auth-link" onClick={() => window.open(resetPasswordUrl, "_blank", "noopener,noreferrer")}>
type="button"
className="auth-link"
onClick={() => window.open(resetPasswordUrl, "_blank", "noopener,noreferrer")}
>
{t('auth.forgotPassword')} {t('auth.forgotPassword')}
</button> </button>
<button type="submit" className="btn-primary btn-full" disabled={loading}> <button type="submit" className="btn-primary btn-full" disabled={loading}>
{loading ? "..." : t('auth.signIn')} {loading ? "..." : t('auth.signIn')}
</button> </button>
{needsEmailVerification ? ( {needsEmailVerification && (
<div className="auth-notice auth-notice-warn" style={{ marginTop: 10 }}> <div className="auth-notice auth-notice-warn" style={{ marginTop: 10 }}>
{t('auth.verifyEmail')} {t('auth.verifyEmail')}
</div> </div>
) : null} )}
{ssoEnabled ? ( {ssoEnabled && (
<> <>
<div className="auth-divider">{t('auth.orContinueWith')}</div> <div className="auth-divider">{t('auth.orContinueWith')}</div>
<div className="auth-oauth"> <div className="auth-oauth">
<button <button type="button" className="btn-oauth btn-oauth-google" disabled={loading}
type="button" onClick={() => { trackEvent("login_provider_click", { provider: "google" }); loginWithProvider && loginWithProvider("google"); }}>
className="btn-oauth btn-oauth-google"
disabled={loading}
onClick={() => {
trackEvent("login_provider_click", { provider: "google" });
loginWithProvider && loginWithProvider("google");
}}
>
<i className="fa-brands fa-google"></i> <i className="fa-brands fa-google"></i>
<span>{t('auth.continueWithGoogle')}</span> <span>{t('auth.continueWithGoogle')}</span>
</button> </button>
<button <button type="button" className="btn-oauth btn-oauth-facebook" disabled={loading}
type="button" onClick={() => { trackEvent("login_provider_click", { provider: "facebook" }); loginWithProvider && loginWithProvider("facebook"); }}>
className="btn-oauth btn-oauth-facebook"
disabled={loading}
onClick={() => {
trackEvent("login_provider_click", { provider: "facebook" });
loginWithProvider && loginWithProvider("facebook");
}}
>
<i className="fa-brands fa-facebook-f"></i> <i className="fa-brands fa-facebook-f"></i>
<span>{t('auth.continueWithFacebook')}</span> <span>{t('auth.continueWithFacebook')}</span>
</button> </button>
</div> </div>
</> </>
) : null} )}
</form> </form>
) : ( ) : (
<form className="auth-form" onSubmit={handleSignupSubmit}> <form className="auth-form" onSubmit={handleSignupSubmit}>
@ -786,9 +712,7 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
{t('auth.password')} {t('auth.password')}
<input type="password" value={regPass} onChange={(e) => setRegPass(e.target.value)} /> <input type="password" value={regPass} onChange={(e) => setRegPass(e.target.value)} />
</label> </label>
{signupError && <div className="auth-notice auth-notice-error">{signupError}</div>}
{signupError ? <div className="auth-notice auth-notice-error">{signupError}</div> : null}
<button type="submit" className="btn-primary btn-full" disabled={signupLoading}> <button type="submit" className="btn-primary btn-full" disabled={signupLoading}>
{signupLoading ? "..." : t('auth.createAccount')} {signupLoading ? "..." : t('auth.createAccount')}
</button> </button>
@ -796,28 +720,22 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
)} )}
</div> </div>
</div> </div>
) : null} )}
{showUsernameModal ? ( {/* Username Modal */}
{showUsernameModal && (
<div className="auth-modal-backdrop" onClick={() => {}}> <div className="auth-modal-backdrop" onClick={() => {}}>
<div className="auth-modal" onClick={(e) => e.stopPropagation()}> <div className="auth-modal" onClick={(e) => e.stopPropagation()}>
<div className="auth-modal-header"> <div className="auth-modal-header">
<div className="auth-tab auth-tab-active">{t('auth.chooseUsername')}</div> <div className="auth-tab auth-tab-active">{t('auth.chooseUsername')}</div>
</div> </div>
<div className="auth-notice auth-notice-info"> <div className="auth-notice auth-notice-info">{t('auth.pickUsername')}</div>
{t('auth.pickUsername')} {usernameError && <div className="auth-notice auth-notice-error">{usernameError}</div>}
</div> {usernameInfo && <div className="auth-notice auth-notice-success">{usernameInfo}</div>}
{usernameError ? <div className="auth-notice auth-notice-error">{usernameError}</div> : null}
{usernameInfo ? <div className="auth-notice auth-notice-success">{usernameInfo}</div> : null}
<form className="auth-form" onSubmit={handleUsernameSave}> <form className="auth-form" onSubmit={handleUsernameSave}>
<label> <label>
{t('auth.username')} {t('auth.username')}
<input <input type="text" value={desiredUsername} onChange={(e) => setDesiredUsername(e.target.value)} autoComplete="username" />
type="text"
value={desiredUsername}
onChange={(e) => setDesiredUsername(e.target.value)}
autoComplete="username"
/>
</label> </label>
<button type="submit" className="btn-primary btn-full" disabled={usernameSaving}> <button type="submit" className="btn-primary btn-full" disabled={usernameSaving}>
{usernameSaving ? t('auth.savingUsername') : t('auth.saveUsername')} {usernameSaving ? t('auth.savingUsername') : t('auth.saveUsername')}
@ -825,16 +743,13 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
</form> </form>
</div> </div>
</div> </div>
) : null} )}
{/* Friends Panel - dropdown from topbar */} {/* Friends Panel */}
{showFriends && ( {showFriends && (
<div className="friends-dropdown-wrap"> <div className="friends-dropdown-wrap">
<FriendsList <FriendsList
onSelectUser={(uname) => { onSelectUser={(uname) => { setShowFriends(false); if (onUserIsland) onUserIsland(uname); }}
setShowFriends(false);
if (onUserIsland) onUserIsland(uname);
}}
onClose={() => setShowFriends(false)} onClose={() => setShowFriends(false)}
/> />
</div> </div>

View File

@ -207,13 +207,13 @@ function UserChip({ authenticated, loading, username, avatarUrl, onClick }) {
whileTap={{ scale: 0.98 }} whileTap={{ scale: 0.98 }}
> >
{/* Avatar */} {/* Avatar */}
<div className="w-7 h-7 rounded-full overflow-hidden <div className="w-6 h-6 sm:w-7 sm:h-7 rounded-full overflow-hidden
bg-gradient-to-br from-blue-500 via-cyan-500 to-blue-400 p-0.5"> bg-gradient-to-br from-blue-500 via-cyan-500 to-blue-400 p-0.5">
{avatarUrl ? ( {avatarUrl ? (
<img src={toSmallImageUrl(avatarUrl)} alt={username} className="w-full h-full rounded-full object-cover bg-surface-900" /> <img src={toSmallImageUrl(avatarUrl)} alt={username} className="w-full h-full rounded-full object-cover bg-surface-900" />
) : ( ) : (
<div className="w-full h-full rounded-full bg-surface-800 flex items-center justify-center text-white font-bold text-xs"> <div className="w-full h-full rounded-full bg-surface-800 flex items-center justify-center text-white font-bold text-[10px] sm:text-xs">
{authenticated ? initialLetter(username) : <i className="fa-solid fa-user text-[10px]" />} {authenticated ? initialLetter(username) : <i className="fa-solid fa-user text-[8px] sm:text-[10px]" />}
</div> </div>
)} )}
</div> </div>
@ -235,7 +235,7 @@ function UserChip({ authenticated, loading, username, avatarUrl, onClick }) {
// Main Component // Main Component
// //
export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland }) { export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClick, onUserIsland, onMyLocation, onSearch }) {
const { t, i18n } = useTranslation(); const { t, i18n } = useTranslation();
const { const {
loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled, loading, authenticated, username, login, logout, restore, loginWithProvider, ssoEnabled,
@ -257,6 +257,7 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
const [showFriends, setShowFriends] = useState(false); const [showFriends, setShowFriends] = useState(false);
const [pendingRequestsCount, setPendingRequestsCount] = useState(0); const [pendingRequestsCount, setPendingRequestsCount] = useState(0);
const [showInstallBtn, setShowInstallBtn] = useState(false); const [showInstallBtn, setShowInstallBtn] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
// Form states // Form states
const [loginUser, setLoginUser] = useState(""); const [loginUser, setLoginUser] = useState("");
@ -387,6 +388,13 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
}, [authenticated, username, showUsernameModal, loading, needsUsername]); }, [authenticated, username, showUsernameModal, loading, needsUsername]);
// Handlers // Handlers
const handleSearchSubmit = (e) => {
e.preventDefault();
if (searchQuery.trim() && onSearch) {
onSearch(searchQuery.trim());
}
};
const handleLoginSubmit = async (e) => { const handleLoginSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
trackEvent("login_submit"); trackEvent("login_submit");
@ -503,19 +511,17 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
animate={{ opacity: [0.3, 0.6, 0.3] }} animate={{ opacity: [0.3, 0.6, 0.3] }}
transition={{ duration: 2, repeat: Infinity }} transition={{ duration: 2, repeat: Infinity }}
/> />
<div className="relative w-10 h-10 rounded-xl bg-gradient-to-br from-blue-500 via-cyan-500 to-emerald-500 p-[2px] shadow-lg shadow-cyan-500/25"> <div className="relative w-8 h-8 sm:w-10 sm:h-10 md:w-11 md:h-11 lg:w-12 lg:h-12 rounded-lg sm:rounded-xl bg-gradient-to-br from-blue-500 via-cyan-500 to-emerald-500 shadow-lg shadow-cyan-500/25 flex items-center justify-center flex-shrink-0">
<div className="w-full h-full rounded-[10px] bg-surface-900 flex items-center justify-center"> <img src="/logo.png" alt="SocioWire" className="w-[130%] h-[130%] max-w-none object-cover" style={{ marginTop: '5px', marginLeft: '2px' }} />
<img src="/logo.png" alt="S" className="w-6 h-6 object-contain" />
</div> </div>
</div> </div>
</div> <div className="flex flex-col min-w-0">
<div className="hidden sm:flex flex-col"> <span className="text-xs sm:text-sm md:text-lg font-black text-transparent bg-clip-text bg-gradient-to-r from-white via-blue-100 to-cyan-200 truncate">
<span className="text-lg font-black text-transparent bg-clip-text bg-gradient-to-r from-white via-blue-100 to-cyan-200">
SOCIOWIRE SOCIOWIRE
</span> </span>
<span className="flex items-center gap-1.5 text-[10px] font-medium text-surface-400"> <span className="hidden sm:flex items-center gap-1 sm:gap-1.5 text-[8px] sm:text-[10px] font-medium text-surface-400">
<motion.span <motion.span
className="w-2 h-2 rounded-full bg-emerald-500" className="w-1.5 h-1.5 sm:w-2 sm:h-2 rounded-full bg-emerald-500"
animate={{ scale: [1, 1.3, 1], opacity: [1, 0.7, 1] }} animate={{ scale: [1, 1.3, 1], opacity: [1, 0.7, 1] }}
transition={{ duration: 1.5, repeat: Infinity }} transition={{ duration: 1.5, repeat: Infinity }}
/> />
@ -524,23 +530,8 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</div> </div>
</motion.div> </motion.div>
{/* Center: Theme pills (desktop) */} {/* Spacer */}
<div className="hidden md:flex items-center gap-1 px-1.5 py-1 rounded-full bg-surface-800/40 border border-white/5"> <div className="flex-1" />
{THEMES.map((themeKey) => (
<motion.button
key={themeKey}
className={`px-4 py-1.5 rounded-full text-xs font-bold uppercase tracking-wider transition-all
${theme === themeKey
? "bg-gradient-to-r from-blue-500 to-cyan-500 text-white shadow-lg shadow-cyan-500/25"
: "text-surface-400 hover:text-white hover:bg-white/5"}`}
onClick={() => onChangeTheme?.(themeKey)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{t(`theme.${themeKey}`)}
</motion.button>
))}
</div>
{/* Right: Action buttons */} {/* Right: Action buttons */}
<div className="flex items-center gap-1.5 sm:gap-2"> <div className="flex items-center gap-1.5 sm:gap-2">
@ -556,14 +547,15 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
<i className="fa-solid fa-download" /> <i className="fa-solid fa-download" />
<span className="hidden sm:inline">{t('topbar.install')}</span> <span className="hidden sm:inline">{t('topbar.installApp')}</span>
</motion.button> </motion.button>
)} )}
{/* Theme (mobile) */}
<div className="md:hidden" ref={themeBtnRef}> {/* Theme */}
<div ref={themeBtnRef}>
<motion.button <motion.button
className={`w-9 h-9 rounded-xl flex items-center justify-center transition-all className={`w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-lg sm:rounded-xl flex items-center justify-center transition-all text-xs sm:text-sm
${showThemeMenu ? "bg-cyan-500/20 text-cyan-400 border-cyan-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`} ${showThemeMenu ? "bg-cyan-500/20 text-cyan-400 border-cyan-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`}
onClick={toggleThemeMenu} onClick={toggleThemeMenu}
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.1 }}
@ -576,7 +568,7 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
{/* Language */} {/* Language */}
<div ref={langBtnRef}> <div ref={langBtnRef}>
<motion.button <motion.button
className={`w-9 h-9 rounded-xl flex items-center justify-center transition-all className={`w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-lg sm:rounded-xl flex items-center justify-center transition-all text-xs sm:text-sm
${showLangMenu ? "bg-blue-500/20 text-blue-400 border-blue-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`} ${showLangMenu ? "bg-blue-500/20 text-blue-400 border-blue-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`}
onClick={toggleLangMenu} onClick={toggleLangMenu}
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.1 }}
@ -587,19 +579,19 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</div> </div>
{/* Friends */} {/* Friends */}
{authenticated && (
<motion.button <motion.button
className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-all className={`relative w-7 h-7 sm:w-8 sm:h-8 md:w-9 md:h-9 rounded-lg sm:rounded-xl flex items-center justify-center transition-all text-xs sm:text-sm
${showFriends ? "bg-amber-500/20 text-amber-400 border-amber-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`} ${showFriends ? "bg-amber-500/20 text-amber-400 border-amber-500/40" : "bg-surface-800/50 text-surface-400 border-white/5"} border`}
onClick={() => setShowFriends(!showFriends)} onClick={() => authenticated ? setShowFriends(!showFriends) : openModal("login")}
whileHover={{ scale: 1.1 }} whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }} whileTap={{ scale: 0.9 }}
title={t('topbar.friends')}
> >
<i className="fa-solid fa-user-group" /> <i className="fa-solid fa-user-group" />
{pendingRequestsCount > 0 && ( {authenticated && pendingRequestsCount > 0 && (
<motion.span <motion.span
className="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1 className="absolute -top-1 -right-1 sm:-top-1.5 sm:-right-1.5 min-w-[14px] sm:min-w-[18px] h-[14px] sm:h-[18px] px-0.5 sm:px-1
rounded-full bg-gradient-to-r from-red-500 to-pink-500 text-white text-[10px] font-bold rounded-full bg-gradient-to-r from-red-500 to-pink-500 text-white text-[8px] sm:text-[10px] font-bold
flex items-center justify-center shadow-lg shadow-red-500/50" flex items-center justify-center shadow-lg shadow-red-500/50"
initial={{ scale: 0 }} initial={{ scale: 0 }}
animate={{ scale: 1 }} animate={{ scale: 1 }}
@ -608,7 +600,6 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</motion.span> </motion.span>
)} )}
</motion.button> </motion.button>
)}
{/* Separator */} {/* Separator */}
<div className="hidden sm:block w-px h-7 bg-gradient-to-b from-transparent via-white/20 to-transparent mx-1" /> <div className="hidden sm:block w-px h-7 bg-gradient-to-b from-transparent via-white/20 to-transparent mx-1" />
@ -651,7 +642,7 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
className="absolute inset-0 bg-gradient-to-r from-emerald-400 to-teal-400 opacity-0 group-hover:opacity-100 transition-opacity" className="absolute inset-0 bg-gradient-to-r from-emerald-400 to-teal-400 opacity-0 group-hover:opacity-100 transition-opacity"
/> />
<i className="fa-solid fa-user-plus relative" /> <i className="fa-solid fa-user-plus relative" />
<span className="relative hidden sm:inline">{t('topbar.joinNow')}</span> <span className="relative hidden sm:inline">{t('topbar.login')}</span>
</motion.button> </motion.button>
)} )}
</div> </div>

View File

@ -505,13 +505,17 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
}, [postId]); }, [postId]);
// Fetch comments // Fetch comments
const refreshComments = useCallback(async () => { const refreshComments = useCallback(async (autoOpen = false) => {
if (!postId) return; if (!postId) return;
const items = await fetchPostComments(postId, 50); const items = await fetchPostComments(postId, 50);
if (Array.isArray(items)) { setComments(items); setCounts(c => ({ ...c, comments: items.length })); } if (Array.isArray(items)) {
setComments(items);
setCounts(c => ({ ...c, comments: items.length }));
if (autoOpen && items.length > 0) setShowComments(true);
}
}, [postId]); }, [postId]);
useEffect(() => { refreshComments(); }, [refreshComments]); useEffect(() => { refreshComments(true); }, [refreshComments]);
useEffect(() => { if (showComments) commentsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [comments, showComments]); useEffect(() => { if (showComments) commentsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [comments, showComments]);
// Handlers // Handlers
@ -545,7 +549,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
setCommentInput(""); setCommentInput("");
setCommentSending(true); setCommentSending(true);
const tempId = `temp_${Date.now()}`; const tempId = `temp_${Date.now()}`;
const newComment = { id: tempId, body, author: username || "You", created_at: new Date().toISOString(), _pending: true }; const newComment = { id: tempId, body, username: username || "You", created_at: new Date().toISOString(), _pending: true };
setComments(prev => [...prev, newComment]); setComments(prev => [...prev, newComment]);
setCounts(c => ({ ...c, comments: c.comments + 1 })); setCounts(c => ({ ...c, comments: c.comments + 1 }));
const res = await createPostComment(postId, body); const res = await createPostComment(postId, body);
@ -764,28 +768,28 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
</div> </div>
{/* Truth Score */} {/* Truth Score */}
<div className="mb-4 p-4 rounded-2xl bg-themed-elevated"> <div className="mb-3 p-2.5 rounded-xl bg-themed-elevated">
<div className="flex items-center gap-4"> <div className="flex items-center gap-3">
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={() => handleTruthVote("false")} <motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={() => handleTruthVote("false")}
className="w-12 h-12 rounded-full bg-red-500/20 border border-red-500/30 text-red-400 flex items-center justify-center hover:bg-red-500/30"> className="w-9 h-9 rounded-full bg-red-500/20 border border-red-500/30 text-red-400 flex items-center justify-center hover:bg-red-500/30">
<i className="fa-solid fa-thumbs-down text-lg" /> <i className="fa-solid fa-thumbs-down text-sm" />
</motion.button> </motion.button>
<div className="flex-1"> <div className="flex-1">
<div className="flex items-center justify-center gap-2 mb-2"> <div className="flex items-center justify-center gap-1.5 mb-1">
<span className={`text-3xl font-black ${truth.score >= 70 ? 'text-emerald-400' : truth.score >= 50 ? 'text-amber-400' : 'text-red-400'}`}> <span className={`text-xl font-black ${truth.score >= 70 ? 'text-emerald-400' : truth.score >= 50 ? 'text-amber-400' : 'text-red-400'}`}>
{Math.round(truth.score)} {Math.round(truth.score)}
</span> </span>
<span className="text-themed-muted">/ 100</span> <span className="text-themed-muted text-xs">/ 100</span>
</div> </div>
<div className="h-2.5 rounded-full bg-surface-800 overflow-hidden"> <div className="h-1.5 rounded-full bg-surface-800 overflow-hidden">
<motion.div className={`h-full rounded-full ${truth.score >= 70 ? 'bg-gradient-to-r from-emerald-500 to-teal-400' : truth.score >= 50 ? 'bg-gradient-to-r from-amber-500 to-orange-400' : 'bg-gradient-to-r from-red-500 to-pink-400'}`} <motion.div className={`h-full rounded-full ${truth.score >= 70 ? 'bg-gradient-to-r from-emerald-500 to-teal-400' : truth.score >= 50 ? 'bg-gradient-to-r from-amber-500 to-orange-400' : 'bg-gradient-to-r from-red-500 to-pink-400'}`}
initial={{ width: 0 }} animate={{ width: `${truth.score}%` }} transition={{ duration: 0.6 }} /> initial={{ width: 0 }} animate={{ width: `${truth.score}%` }} transition={{ duration: 0.6 }} />
</div> </div>
<p className="text-center text-xs text-themed-muted mt-1">{truth.count} votes</p> <p className="text-center text-[10px] text-themed-muted mt-0.5">{truth.count} votes</p>
</div> </div>
<motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={() => handleTruthVote("true")} <motion.button whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} onClick={() => handleTruthVote("true")}
className="w-12 h-12 rounded-full bg-emerald-500/20 border border-emerald-500/30 text-emerald-400 flex items-center justify-center hover:bg-emerald-500/30"> className="w-9 h-9 rounded-full bg-emerald-500/20 border border-emerald-500/30 text-emerald-400 flex items-center justify-center hover:bg-emerald-500/30">
<i className="fa-solid fa-thumbs-up text-lg" /> <i className="fa-solid fa-thumbs-up text-sm" />
</motion.button> </motion.button>
</div> </div>
</div> </div>
@ -834,12 +838,19 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
comments.map((c) => ( comments.map((c) => (
<motion.div key={c.id} className={`p-3 rounded-xl bg-themed-primary border border-themed ${c._pending ? "opacity-50" : ""} ${c._failed ? "border-red-500/30" : ""}`} <motion.div key={c.id} className={`p-3 rounded-xl bg-themed-primary border border-themed ${c._pending ? "opacity-50" : ""} ${c._failed ? "border-red-500/30" : ""}`}
initial={{ opacity: 0, y: 10 }} animate={{ opacity: c._pending ? 0.5 : 1, y: 0 }}> initial={{ opacity: 0, y: 10 }} animate={{ opacity: c._pending ? 0.5 : 1, y: 0 }}>
<div className="flex items-center gap-2 mb-1"> <div className="flex items-start gap-2">
<span className="text-xs font-bold text-accent">@{c.author}</span> <div className="w-8 h-8 rounded-full bg-gradient-to-br from-accent to-blue-500 flex items-center justify-center text-white text-xs font-bold flex-shrink-0">
{(c.username || "?")[0]?.toUpperCase()}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-0.5">
<span className="text-xs font-bold text-accent">@{c.username || "user"}</span>
<span className="text-xs text-themed-muted">{relativeTime(c.created_at, t)}</span> <span className="text-xs text-themed-muted">{relativeTime(c.created_at, t)}</span>
{c._failed && <span className="text-xs text-red-400">Failed</span>} {c._failed && <span className="text-xs text-red-400">Failed</span>}
</div> </div>
<p className="text-sm text-themed-secondary">{c.body}</p> <p className="text-sm text-themed-secondary">{c.body}</p>
</div>
</div>
</motion.div> </motion.div>
)) ))
)} )}

File diff suppressed because it is too large Load Diff