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",
"private": true,
"version": "0.0.75",
"version": "0.0.87",
"type": "module",
"scripts": {
"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 }),
});
const data = await res.json().catch(() => ({}));
return { ok: res.ok, ...data };
return { ...data, ok: res.ok };
} catch {
return { ok: false };
}

View File

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

View File

@ -207,13 +207,13 @@ function UserChip({ authenticated, loading, username, avatarUrl, onClick }) {
whileTap={{ scale: 0.98 }}
>
{/* 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">
{avatarUrl ? (
<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">
{authenticated ? initialLetter(username) : <i className="fa-solid fa-user text-[10px]" />}
<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-[8px] sm:text-[10px]" />}
</div>
)}
</div>
@ -235,7 +235,7 @@ function UserChip({ authenticated, loading, username, avatarUrl, onClick }) {
// 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 {
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 [pendingRequestsCount, setPendingRequestsCount] = useState(0);
const [showInstallBtn, setShowInstallBtn] = useState(false);
const [searchQuery, setSearchQuery] = useState("");
// Form states
const [loginUser, setLoginUser] = useState("");
@ -387,6 +388,13 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
}, [authenticated, username, showUsernameModal, loading, needsUsername]);
// Handlers
const handleSearchSubmit = (e) => {
e.preventDefault();
if (searchQuery.trim() && onSearch) {
onSearch(searchQuery.trim());
}
};
const handleLoginSubmit = async (e) => {
e.preventDefault();
trackEvent("login_submit");
@ -503,19 +511,17 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
animate={{ opacity: [0.3, 0.6, 0.3] }}
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="w-full h-full rounded-[10px] bg-surface-900 flex items-center justify-center">
<img src="/logo.png" alt="S" className="w-6 h-6 object-contain" />
</div>
<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">
<img src="/logo.png" alt="SocioWire" className="w-[130%] h-[130%] max-w-none object-cover" style={{ marginTop: '5px', marginLeft: '2px' }} />
</div>
</div>
<div className="hidden sm:flex flex-col">
<span className="text-lg font-black text-transparent bg-clip-text bg-gradient-to-r from-white via-blue-100 to-cyan-200">
<div className="flex flex-col min-w-0">
<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">
SOCIOWIRE
</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
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] }}
transition={{ duration: 1.5, repeat: Infinity }}
/>
@ -524,23 +530,8 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</div>
</motion.div>
{/* Center: Theme pills (desktop) */}
<div className="hidden md:flex items-center gap-1 px-1.5 py-1 rounded-full bg-surface-800/40 border border-white/5">
{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>
{/* Spacer */}
<div className="flex-1" />
{/* Right: Action buttons */}
<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 }}
>
<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>
)}
{/* Theme (mobile) */}
<div className="md:hidden" ref={themeBtnRef}>
{/* Theme */}
<div ref={themeBtnRef}>
<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`}
onClick={toggleThemeMenu}
whileHover={{ scale: 1.1 }}
@ -576,7 +568,7 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
{/* Language */}
<div ref={langBtnRef}>
<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`}
onClick={toggleLangMenu}
whileHover={{ scale: 1.1 }}
@ -587,28 +579,27 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
</div>
{/* Friends */}
{authenticated && (
<motion.button
className={`relative w-9 h-9 rounded-xl flex items-center justify-center transition-all
${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)}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
>
<i className="fa-solid fa-user-group" />
{pendingRequestsCount > 0 && (
<motion.span
className="absolute -top-1.5 -right-1.5 min-w-[18px] h-[18px] px-1
rounded-full bg-gradient-to-r from-red-500 to-pink-500 text-white text-[10px] font-bold
flex items-center justify-center shadow-lg shadow-red-500/50"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
>
{pendingRequestsCount}
</motion.span>
)}
</motion.button>
)}
<motion.button
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`}
onClick={() => authenticated ? setShowFriends(!showFriends) : openModal("login")}
whileHover={{ scale: 1.1 }}
whileTap={{ scale: 0.9 }}
title={t('topbar.friends')}
>
<i className="fa-solid fa-user-group" />
{authenticated && pendingRequestsCount > 0 && (
<motion.span
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-[8px] sm:text-[10px] font-bold
flex items-center justify-center shadow-lg shadow-red-500/50"
initial={{ scale: 0 }}
animate={{ scale: 1 }}
>
{pendingRequestsCount}
</motion.span>
)}
</motion.button>
{/* Separator */}
<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"
/>
<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>
)}
</div>

View File

@ -505,13 +505,17 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
}, [postId]);
// Fetch comments
const refreshComments = useCallback(async () => {
const refreshComments = useCallback(async (autoOpen = false) => {
if (!postId) return;
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]);
useEffect(() => { refreshComments(); }, [refreshComments]);
useEffect(() => { refreshComments(true); }, [refreshComments]);
useEffect(() => { if (showComments) commentsEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [comments, showComments]);
// Handlers
@ -545,7 +549,7 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
setCommentInput("");
setCommentSending(true);
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]);
setCounts(c => ({ ...c, comments: c.comments + 1 }));
const res = await createPostComment(postId, body);
@ -764,28 +768,28 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
</div>
{/* Truth Score */}
<div className="mb-4 p-4 rounded-2xl bg-themed-elevated">
<div className="flex items-center gap-4">
<div className="mb-3 p-2.5 rounded-xl bg-themed-elevated">
<div className="flex items-center gap-3">
<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">
<i className="fa-solid fa-thumbs-down text-lg" />
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-sm" />
</motion.button>
<div className="flex-1">
<div className="flex items-center justify-center gap-2 mb-2">
<span className={`text-3xl font-black ${truth.score >= 70 ? 'text-emerald-400' : truth.score >= 50 ? 'text-amber-400' : 'text-red-400'}`}>
<div className="flex items-center justify-center gap-1.5 mb-1">
<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)}
</span>
<span className="text-themed-muted">/ 100</span>
<span className="text-themed-muted text-xs">/ 100</span>
</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'}`}
initial={{ width: 0 }} animate={{ width: `${truth.score}%` }} transition={{ duration: 0.6 }} />
</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>
<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">
<i className="fa-solid fa-thumbs-up text-lg" />
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-sm" />
</motion.button>
</div>
</div>
@ -834,12 +838,19 @@ export default function PostDetailModal({ post, onClose, onPostUpdated, onPostDe
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" : ""}`}
initial={{ opacity: 0, y: 10 }} animate={{ opacity: c._pending ? 0.5 : 1, y: 0 }}>
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-bold text-accent">@{c.author}</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>}
<div className="flex items-start gap-2">
<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>
{c._failed && <span className="text-xs text-red-400">Failed</span>}
</div>
<p className="text-sm text-themed-secondary">{c.body}</p>
</div>
</div>
<p className="text-sm text-themed-secondary">{c.body}</p>
</motion.div>
))
)}

File diff suppressed because it is too large Load Diff