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:
+34
-10
@@ -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
@@ -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>
|
||||
|
||||
@@ -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 />
|
||||
|
||||
Reference in New Issue
Block a user