diff --git a/drizzle/0006_add_favicon_url.sql b/drizzle/0006_add_favicon_url.sql new file mode 100644 index 0000000..282ff35 --- /dev/null +++ b/drizzle/0006_add_favicon_url.sql @@ -0,0 +1,2 @@ +-- Add favicon_url column to nodes table +ALTER TABLE "nodes" ADD COLUMN IF NOT EXISTS "favicon_url" text; diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 7648bd4..57ab5b1 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -69,6 +69,7 @@ export default function AdminPage() { rules: '', bannerUrl: '', logoUrl: '', + faviconUrl: '', accentColor: '#FFFFFF', isNsfw: false, }); @@ -77,6 +78,8 @@ export default function AdminPage() { const [bannerUploadError, setBannerUploadError] = useState(null); const [isUploadingLogo, setIsUploadingLogo] = useState(false); const [logoUploadError, setLogoUploadError] = useState(null); + const [isUploadingFavicon, setIsUploadingFavicon] = useState(false); + const [faviconUploadError, setFaviconUploadError] = useState(null); useEffect(() => { fetch('/api/admin/me') @@ -136,6 +139,7 @@ export default function AdminPage() { rules: data.rules || '', bannerUrl: data.bannerUrl || '', logoUrl: data.logoUrl || '', + faviconUrl: data.faviconUrl || '', accentColor: data.accentColor || '#FFFFFF', isNsfw: data.isNsfw || false, }); @@ -270,6 +274,41 @@ export default function AdminPage() { } }; + const handleFaviconUpload = async (event: React.ChangeEvent) => { + const file = event.target.files?.[0]; + event.target.value = ''; + if (!file) return; + + setFaviconUploadError(null); + setIsUploadingFavicon(true); + + try { + const formData = new FormData(); + formData.append('file', file); + const res = await fetch('/api/media/upload', { + method: 'POST', + body: formData, + }); + const data = await res.json(); + + if (!res.ok || !data.url) { + throw new Error(data.error || 'Upload failed'); + } + + const nextSettings = { + ...nodeSettings, + faviconUrl: data.media?.url || data.url, + }; + setNodeSettings(nextSettings); + await handleSaveSettings(nextSettings); + } catch (error) { + console.error('Favicon upload failed', error); + setFaviconUploadError('Upload failed. Please try again.'); + } finally { + setIsUploadingFavicon(false); + } + }; + const handleUserAction = async (id: string, action: 'suspend' | 'unsuspend' | 'silence' | 'unsilence') => { const needsReason = action === 'suspend' || action === 'silence'; const reason = needsReason ? window.prompt('Reason (optional):') || '' : ''; @@ -602,6 +641,49 @@ export default function AdminPage() { )} +
+ +

+ The icon shown in browser tabs. Recommended: 32x32 or 64x64 PNG. +

+
+ + {nodeSettings.faviconUrl && ( + + )} + {faviconUploadError && ( + {faviconUploadError} + )} +
+ {nodeSettings.faviconUrl && ( +
+ Custom favicon +
+ )} +
+
) => void; +} + +export default function BlurredVideo({ src, onClick }: BlurredVideoProps) { + const containerRef = useRef(null); + const mainVideoRef = useRef(null); + const bgVideoRef = useRef(null); + + useEffect(() => { + // Sync playback between main and background videos + const mainVideo = mainVideoRef.current; + const bgVideo = bgVideoRef.current; + + if (mainVideo && bgVideo) { + const syncTime = () => { + if (Math.abs(mainVideo.currentTime - bgVideo.currentTime) > 0.1) { + bgVideo.currentTime = mainVideo.currentTime; + } + }; + + mainVideo.addEventListener('seeked', syncTime); + mainVideo.addEventListener('play', () => bgVideo.play()); + mainVideo.addEventListener('pause', () => bgVideo.pause()); + + return () => { + mainVideo.removeEventListener('seeked', syncTime); + }; + } + }, []); + + return ( +
+ {/* Background blurred video */} +
+ ); +} diff --git a/src/components/PostCard.tsx b/src/components/PostCard.tsx index 9218099..62f416c 100644 --- a/src/components/PostCard.tsx +++ b/src/components/PostCard.tsx @@ -8,6 +8,7 @@ import { Post } from '@/lib/types'; import { useAuth } from '@/lib/contexts/AuthContext'; import { useToast } from '@/lib/contexts/ToastContext'; import { VideoEmbed } from '@/components/VideoEmbed'; +import BlurredVideo from '@/components/BlurredVideo'; import { formatFullHandle } from '@/lib/utils/handle'; // Component for link preview image that hides on error @@ -516,19 +517,13 @@ export function PostCard({ post, onLike, onRepost, onComment, onDelete, onHide, return (
{isVideo ? ( -