feat(auth,ui): Add dynamic node branding and NSFW content gating

- Replace hardcoded SynapsisLogo with dynamic Image component for custom node logos
- Add logoUrl support to node info state with proper TypeScript typing
- Implement nodeInfoLoaded state to ensure proper rendering timing
- Add NSFW node detection and content gating for unauthenticated users
- Display custom node logo on login page when available, fallback to default
- Show adult content warning gate on home page for NSFW nodes
- Improve error handling in node info fetch with explicit state updates
- Add EyeOff icon for NSFW content warning display
- Enhance login page layout with minHeight for consistent spacing during load
This commit is contained in:
AskIt
2026-01-26 01:44:51 +01:00
parent ff891af927
commit 091c85080b
3 changed files with 80 additions and 28 deletions
+34 -10
View File
@@ -2,7 +2,7 @@
import { useState, useEffect } from 'react';
import Link from 'next/link';
import { SynapsisLogo } from '@/components/Icons';
import Image from 'next/image';
import { TriangleAlert } from 'lucide-react';
export default function LoginPage() {
@@ -14,7 +14,8 @@ export default function LoginPage() {
const [displayName, setDisplayName] = useState('');
const [error, setError] = useState('');
const [loading, setLoading] = useState(false);
const [nodeInfo, setNodeInfo] = useState({ name: '', description: '' });
const [nodeInfoLoaded, setNodeInfoLoaded] = useState(false);
const [nodeInfo, setNodeInfo] = useState<{ name: string; description: string; logoUrl?: string }>({ name: '', description: '' });
const [handleStatus, setHandleStatus] = useState<'idle' | 'checking' | 'available' | 'taken'>('idle');
// Import specific state
@@ -31,10 +32,14 @@ export default function LoginPage() {
.then(data => {
setNodeInfo({
name: data.name || '',
description: data.description || 'Synapsis is designed to function like a global signal layer rather than a culture-bound platform. Anyone can run their own node and still participate in a shared, interconnected network, with global identity, clean terminology, and a modern interface that feels current rather than experimental. Synapsis aims to be neutral, resilient infrastructure for human and machine discourse, more like a protocol or nervous system than a social club.'
description: data.description || 'Synapsis is designed to function like a global signal layer rather than a culture-bound platform. Anyone can run their own node and still participate in a shared, interconnected network, with global identity, clean terminology, and a modern interface that feels current rather than experimental. Synapsis aims to be neutral, resilient infrastructure for human and machine discourse, more like a protocol or nervous system than a social club.',
logoUrl: data.logoUrl || undefined
});
setNodeInfoLoaded(true);
})
.catch(() => { });
.catch(() => {
setNodeInfoLoaded(true);
});
}, []);
// Handle availability check
@@ -156,12 +161,29 @@ export default function LoginPage() {
}}>
<div style={{ width: '100%', maxWidth: '400px' }}>
{/* Logo */}
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '32px' }}>
<div className="logo" style={{ marginBottom: '4px', fontSize: '32px' }}>
<SynapsisLogo />
<span>Synapsis</span>
</div>
{nodeInfo.name && nodeInfo.name !== 'Synapsis' && (
<div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', marginBottom: '32px', minHeight: '120px' }}>
{nodeInfoLoaded && (
<>
{nodeInfo.logoUrl ? (
<Image
src={nodeInfo.logoUrl}
alt={nodeInfo.name || 'Node logo'}
width={200}
height={60}
style={{ marginBottom: '16px', objectFit: 'contain', maxHeight: '60px', width: 'auto' }}
unoptimized
/>
) : (
<Image
src="/logotext.png"
alt="Synapsis"
width={200}
height={48}
style={{ marginBottom: '16px', objectFit: 'contain' }}
priority
/>
)}
{nodeInfo.name && nodeInfo.name !== 'Synapsis' && !nodeInfo.logoUrl && (
<div style={{ fontSize: '18px', fontWeight: 600, color: 'var(--foreground)', marginBottom: '8px' }}>
{nodeInfo.name}
</div>
@@ -169,6 +191,8 @@ export default function LoginPage() {
<p style={{ color: 'var(--foreground-secondary)', marginTop: '0', textAlign: 'center' }}>
{nodeInfo.description}
</p>
</>
)}
</div>
{/* Mode Switcher */}
+27 -1
View File
@@ -6,6 +6,7 @@ import { useAuth } from '@/lib/contexts/AuthContext';
import { PostCard } from '@/components/PostCard';
import { Compose } from '@/components/Compose';
import { Post } from '@/lib/types';
import { EyeOff } from 'lucide-react';
interface FeedMeta {
score: number;
@@ -23,6 +24,7 @@ export default function Home() {
const [loading, setLoading] = useState(true);
const [replyingTo, setReplyingTo] = useState<Post | null>(null);
const [feedType, setFeedType] = useState<'latest' | 'curated'>('latest');
const [isNsfwNode, setIsNsfwNode] = useState(false);
const [feedMeta, setFeedMeta] = useState<{
algorithm: string;
windowHours: number;
@@ -35,6 +37,16 @@ export default function Home() {
};
} | null>(null);
// Fetch node info to check if NSFW
useEffect(() => {
fetch('/api/node')
.then(res => res.json())
.then(data => {
setIsNsfwNode(data.isNsfw || false);
})
.catch(() => {});
}, []);
const loadFeed = async (type: 'latest' | 'curated') => {
setLoading(true);
try {
@@ -145,7 +157,21 @@ export default function Home() {
</div>
)}
{loading ? (
{/* NSFW node gate for unauthenticated users */}
{!user && isNsfwNode ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
<EyeOff size={48} style={{ marginBottom: '16px', opacity: 0.5 }} />
<p style={{ fontSize: '16px', fontWeight: 500, color: 'var(--foreground-secondary)', marginBottom: '8px' }}>
Adult Content
</p>
<p style={{ fontSize: '14px', maxWidth: '320px', margin: '0 auto 20px' }}>
This node contains adult or sensitive content. You must be 18 or older and signed in to view posts.
</p>
<Link href="/login" className="btn btn-primary">
Sign In to Continue
</Link>
</div>
) : loading ? (
<div style={{ padding: '48px', textAlign: 'center', color: 'var(--foreground-tertiary)' }}>
Loading...
</div>
+9 -7
View File
@@ -11,17 +11,17 @@ import { formatFullHandle } from '@/lib/utils/handle';
export function Sidebar() {
const { user, isAdmin } = useAuth();
const pathname = usePathname();
const [customLogoUrl, setCustomLogoUrl] = useState<string | null>(null);
const [customLogoUrl, setCustomLogoUrl] = useState<string | null | undefined>(undefined);
useEffect(() => {
fetch('/api/node')
.then(res => res.json())
.then(data => {
if (data.logoUrl) {
setCustomLogoUrl(data.logoUrl);
}
setCustomLogoUrl(data.logoUrl || null);
})
.catch(() => {});
.catch(() => {
setCustomLogoUrl(null);
});
}, []);
// Home is exact match
@@ -29,8 +29,8 @@ export function Sidebar() {
return (
<aside className="sidebar">
<Link href="/" className="logo">
{customLogoUrl ? (
<Link href="/" className="logo" style={{ minHeight: '42px' }}>
{customLogoUrl === undefined ? null : customLogoUrl ? (
<img src={customLogoUrl} alt="Logo" style={{ maxWidth: '200px', maxHeight: '50px', objectFit: 'contain' }} />
) : (
<Image src="/logotext.png" alt="Synapsis" width={185} height={42} priority />
@@ -45,10 +45,12 @@ export function Sidebar() {
<SearchIcon />
<span>Explore</span>
</Link>
{user && (
<Link href="/notifications" className={`nav-item ${pathname?.startsWith('/notifications') ? 'active' : ''}`}>
<BellIcon />
<span>Notifications</span>
</Link>
)}
{user && (
<Link href="/settings/bots" className={`nav-item ${pathname?.startsWith('/settings/bots') ? 'active' : ''}`}>
<BotIcon />