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:
parent
37f879556e
commit
68c1fce064
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,137 +425,119 @@ 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>
|
||||
</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>
|
||||
|
||||
{/* Action buttons - separate section */}
|
||||
<div className="topbar-actions">
|
||||
{/* PWA Install button - no dismiss X */}
|
||||
{/* 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>
|
||||
)}
|
||||
</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
|
||||
className="btn-action btn-action-install"
|
||||
type="button"
|
||||
className="sw-topbar-btn sw-topbar-btn-install"
|
||||
onClick={() => promptInstall()}
|
||||
title={t('topbar.installApp')}
|
||||
>
|
||||
<i className="fa-solid fa-download"></i>
|
||||
<i className="fa-solid fa-download" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Theme selector (mobile dropdown) */}
|
||||
<div className="theme-selector-wrap">
|
||||
{/* Theme */}
|
||||
<div className="sw-topbar-dropdown" ref={themeBtnRef}>
|
||||
<button
|
||||
ref={themeBtnRef}
|
||||
className={"btn-action btn-action-theme" + (showThemeMenu ? " is-active" : "")}
|
||||
type="button"
|
||||
onClick={toggleThemeMenu}
|
||||
className={`sw-topbar-btn sw-topbar-btn-theme ${showThemeMenu ? 'is-active' : ''}`}
|
||||
onClick={() => { setShowThemeMenu(!showThemeMenu); setShowLangMenu(false); }}
|
||||
title={t(`theme.${theme}`)}
|
||||
>
|
||||
<i className="fa-solid fa-palette"></i>
|
||||
<i className={`fa-solid ${themeIcon}`} />
|
||||
</button>
|
||||
{showThemeMenu && (
|
||||
<div
|
||||
className="theme-dropdown"
|
||||
style={{ top: themeMenuPos.top, right: themeMenuPos.right }}
|
||||
>
|
||||
<div className="sw-dropdown-menu sw-dropdown-theme">
|
||||
{THEMES.map((themeKey) => (
|
||||
<button
|
||||
key={themeKey}
|
||||
type="button"
|
||||
className={"theme-option" + (theme === themeKey ? " is-active" : "")}
|
||||
onClick={() => {
|
||||
onChangeTheme && onChangeTheme(themeKey);
|
||||
setShowThemeMenu(false);
|
||||
}}
|
||||
className={`sw-dropdown-item ${theme === themeKey ? 'is-active' : ''}`}
|
||||
onClick={() => { 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>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Language selector */}
|
||||
<div className="lang-selector-wrap">
|
||||
{/* Language */}
|
||||
<div className="sw-topbar-dropdown" ref={langBtnRef}>
|
||||
<button
|
||||
ref={langBtnRef}
|
||||
className={"btn-action btn-action-lang" + (showLangMenu ? " is-active" : "")}
|
||||
type="button"
|
||||
onClick={toggleLangMenu}
|
||||
className={`sw-topbar-btn sw-topbar-btn-lang ${showLangMenu ? 'is-active' : ''}`}
|
||||
onClick={() => { setShowLangMenu(!showLangMenu); setShowThemeMenu(false); }}
|
||||
title={t('language.selectLanguage')}
|
||||
>
|
||||
<i className="fa-solid fa-globe"></i>
|
||||
<span className="btn-label">{i18n.language?.toUpperCase().slice(0, 2)}</span>
|
||||
<span className="sw-lang-code">{i18n.language?.toUpperCase().slice(0, 2)}</span>
|
||||
</button>
|
||||
{showLangMenu && (
|
||||
<div
|
||||
className="lang-dropdown"
|
||||
style={{ top: langMenuPos.top, right: langMenuPos.right }}
|
||||
>
|
||||
<div className="sw-dropdown-menu sw-dropdown-lang">
|
||||
{SUPPORTED_LANGS.map((lang) => (
|
||||
<button
|
||||
key={lang}
|
||||
type="button"
|
||||
className={"lang-option" + (i18n.language?.startsWith(lang) ? " is-active" : "")}
|
||||
className={`sw-dropdown-item ${i18n.language?.startsWith(lang) ? 'is-active' : ''}`}
|
||||
onClick={() => changeLanguage(lang)}
|
||||
>
|
||||
{t(`language.${lang}`)}
|
||||
|
|
@ -624,44 +547,78 @@ export default function TopBar({ theme = "dark", onChangeTheme, onIslandsClick,
|
|||
)}
|
||||
</div>
|
||||
|
||||
{/* Friends button - only when authenticated */}
|
||||
{/* Friends - only when authenticated */}
|
||||
{authenticated && (
|
||||
<button
|
||||
className={"btn-action btn-action-friends" + (showFriends ? " is-active" : "")}
|
||||
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"></i>
|
||||
<i className="fa-solid fa-user-group" />
|
||||
{pendingRequestsCount > 0 && (
|
||||
<span className="friends-badge-count">{pendingRequestsCount}</span>
|
||||
<span className="sw-badge">{pendingRequestsCount}</span>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Login/Logout button */}
|
||||
{/* User / Login */}
|
||||
<div className="sw-topbar-dropdown" ref={userMenuRef}>
|
||||
<button
|
||||
className="btn-action btn-action-auth"
|
||||
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>
|
||||
|
|
|
|||
|
|
@ -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 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>
|
||||
<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,19 +579,19 @@ 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
|
||||
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={() => setShowFriends(!showFriends)}
|
||||
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" />
|
||||
{pendingRequestsCount > 0 && (
|
||||
{authenticated && 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
|
||||
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 }}
|
||||
|
|
@ -608,7 +600,6 @@ export default function TopBarNew({ theme = "dark", onChangeTheme, onIslandsClic
|
|||
</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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
<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>
|
||||
</motion.div>
|
||||
))
|
||||
)}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue