diff --git a/packages/ui/src/components/ImageLightbox.tsx b/packages/ui/src/components/ImageLightbox.tsx index 491db252..0cee39e6 100644 --- a/packages/ui/src/components/ImageLightbox.tsx +++ b/packages/ui/src/components/ImageLightbox.tsx @@ -1,4 +1,5 @@ import { useState, useEffect, useRef } from 'react'; +import { createPortal } from 'react-dom'; import { TransformWrapper, TransformComponent } from 'react-zoom-pan-pinch'; import { ArrowUp, ArrowDown, Upload, Info, FileText, Sparkles, Mic, MicOff, Plus, Trash2, Save, History, Wand2 } from 'lucide-react'; import { Button } from '@/components/ui/button'; @@ -266,11 +267,8 @@ export default function ImageLightbox({ alt={imageTitle} sizes={`${Math.ceil(scale * 100)}vw`} responsiveSizes={[640, 1024]} - imgClassName={isMobile - ? "max-w-[100dvw] max-h-[100dvh] object-contain pointer-events-auto" - : "max-w-[90vw] max-h-[90vh] object-contain cursor-grab active:cursor-grabbing pointer-events-auto" - } - className="w-full h-full flex items-center justify-center" + imgClassName="max-w-[100vw] max-h-[100dvh] object-contain pointer-events-auto md:max-w-[90vw] md:max-h-[90vh] md:cursor-grab md:active:cursor-grabbing" + className="flex items-center justify-center" loading="eager" draggable={false} onLoad={() => setLightboxLoaded(true)} @@ -292,7 +290,7 @@ export default function ImageLightbox({ /> ); - return ( + return createPortal(
{ @@ -320,8 +318,8 @@ export default function ImageLightbox({ } }} > -
-
+
+
{isMobile ? responsiveImageEl : ( -
+
, + document.body ); } diff --git a/packages/ui/src/components/MarkdownRenderer.tsx b/packages/ui/src/components/MarkdownRenderer.tsx index 5ad7c9f6..ea6b9df8 100644 --- a/packages/ui/src/components/MarkdownRenderer.tsx +++ b/packages/ui/src/components/MarkdownRenderer.tsx @@ -1,5 +1,6 @@ import React, { useMemo, useEffect, useRef, useState, Suspense } from 'react'; import ReactMarkdown from 'react-markdown'; +import remarkGfm from 'remark-gfm'; import HashtagText from './HashtagText'; import Prism from 'prismjs'; import ResponsiveImage from './ResponsiveImage'; @@ -166,6 +167,7 @@ const MarkdownRenderer = React.memo(({ content, className = "", variables }: Mar className={`prose prose-sm max-w-none dark:prose-invert ${className}`} > { // Basic implementation of ResponsiveImage @@ -245,6 +247,20 @@ const MarkdownRenderer = React.memo(({ content, className = "", variables }: Mar } return

{children}

; }, + table: ({ node, ...props }) => ( +
+ + + ), + thead: ({ node, ...props }) => ( + + ), + th: ({ node, ...props }) => ( +
+ ), + td: ({ node, ...props }) => ( + + ), }} > {finalContent} diff --git a/packages/ui/src/components/PhotoCard.tsx b/packages/ui/src/components/PhotoCard.tsx index 38f0352b..8d48411b 100644 --- a/packages/ui/src/components/PhotoCard.tsx +++ b/packages/ui/src/components/PhotoCard.tsx @@ -73,7 +73,7 @@ const PhotoCard = ({ apiUrl, versionCount, isExternal = false, - imageFit = 'cover', + imageFit = 'contain', className, preset }: PhotoCardProps) => { @@ -335,7 +335,7 @@ const PhotoCard = ({ return (
{/* Image */} diff --git a/packages/ui/src/components/ResponsiveImage.tsx b/packages/ui/src/components/ResponsiveImage.tsx index e712edfd..3a033d3f 100644 --- a/packages/ui/src/components/ResponsiveImage.tsx +++ b/packages/ui/src/components/ResponsiveImage.tsx @@ -29,13 +29,16 @@ export interface ResponsiveData { stats?: any; } -const ResponsiveImage: React.FC = ({ +const DEFAULT_RESPONSIVE_SIZES = [640, 1280]; +const DEFAULT_FORMATS = ['avif']; + +const ResponsiveImage: React.FC = React.memo(({ src, sizes = '(max-width: 1024px) 100vw, 50vw', className, imgClassName, - responsiveSizes = [640, 1280], - formats = ['avif'], + responsiveSizes = DEFAULT_RESPONSIVE_SIZES, + formats = DEFAULT_FORMATS, alt, onDataLoaded, rootMargin = '800px', @@ -89,6 +92,7 @@ const ResponsiveImage: React.FC = ({ useEffect(() => { if (data && onDataLoaded) { + console.log('onDataLoaded data ', data) onDataLoaded(data); } }, [data, onDataLoaded]); @@ -100,10 +104,14 @@ const ResponsiveImage: React.FC = ({ // Check if image is already loaded (from cache) useEffect(() => { - if (imgRef.current?.complete) { + const img = imgRef.current; + if (img?.complete && img.naturalWidth > 0) { setImgLoaded(true); } - }, [data, imgLoaded]); + }, [data]); + + + console.log('class name ', className, data) // If we are enabled (isInView) but have no data and no error yet, // we are effectively in a "pending load" state. @@ -124,7 +132,7 @@ const ResponsiveImage: React.FC = ({ } return ( -
+
{(data.sources || []).map((source, index) => ( @@ -158,6 +166,17 @@ const ResponsiveImage: React.FC = ({ )}
); -}; +}, (prev, next) => { + // Only compare props that affect visual output — ignore callbacks + return prev.src === next.src && + prev.sizes === next.sizes && + prev.className === next.className && + prev.imgClassName === next.imgClassName && + prev.alt === next.alt && + prev.loading === next.loading && + prev.data === next.data && + prev.responsiveSizes === next.responsiveSizes && + prev.formats === next.formats; +}); export default ResponsiveImage; diff --git a/packages/ui/src/hooks/useResponsiveImage.ts b/packages/ui/src/hooks/useResponsiveImage.ts index 0ed99b21..e9c4c4bd 100644 --- a/packages/ui/src/hooks/useResponsiveImage.ts +++ b/packages/ui/src/hooks/useResponsiveImage.ts @@ -1,5 +1,5 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } from 'react'; import { ResponsiveData } from '@/components/ResponsiveImage'; interface UseResponsiveImageProps { @@ -10,14 +10,17 @@ interface UseResponsiveImageProps { apiUrl?: string; } +const DEFAULT_SIZES = [180, 640, 1024]; +const DEFAULT_FORMATS = ['avif', 'webp', 'jpeg']; + // Module-level cache to deduplicate requests // Key: stringified request params, Value: Promise const requestCache = new Map>(); export const useResponsiveImage = ({ src, - responsiveSizes = [180, 640, 1024], - formats = ['avif', 'webp', 'jpeg'], + responsiveSizes = DEFAULT_SIZES, + formats = DEFAULT_FORMATS, enabled = true, apiUrl, }: UseResponsiveImageProps) => { @@ -25,6 +28,10 @@ export const useResponsiveImage = ({ const [loading, setLoading] = useState(false); const [error, setError] = useState(null); + // Stable key for deps — only changes when array contents actually change + const sizesKey = useMemo(() => JSON.stringify(responsiveSizes), [responsiveSizes]); + const formatsKey = useMemo(() => JSON.stringify(formats), [formats]); + useEffect(() => { let isMounted = true; const generateResponsiveImages = async () => { @@ -139,7 +146,7 @@ export const useResponsiveImage = ({ return () => { isMounted = false; }; - }, [src, JSON.stringify(responsiveSizes), JSON.stringify(formats), enabled]); + }, [src, sizesKey, formatsKey, enabled]); return { data, loading, error }; }; diff --git a/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx b/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx index 08555624..d582cead 100644 --- a/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx +++ b/packages/ui/src/modules/pages/editor/UserPageTypeFields.tsx @@ -51,14 +51,13 @@ export const UserPageTypeFields: React.FC = ({ } }, [pageMeta]); - const handleFormChange = (typeId: string, data: any) => { + const handleFormChange = async (typeId: string, data: any) => { setFormData(prev => ({ ...prev, [typeId]: data })); - }; - const handleFormSubmit = async (typeId: string, data: any) => { + // Fire command immediately so page.meta stays in sync for global save const newTypeValues = { ...(pageMeta?.typeValues || {}), [typeId]: data @@ -75,10 +74,9 @@ export const UserPageTypeFields: React.FC = ({ { meta: newMeta }, (updatedData) => { if (onMetaUpdate) onMetaUpdate(updatedData.meta); - toast.success("Fields updated"); } ); - + console.log("command", command); await executeCommand(command); }; @@ -188,17 +186,11 @@ export const UserPageTypeFields: React.FC = ({ widgets={customWidgets} templates={customTemplates} onChange={(e) => isEditMode && handleFormChange(type.id, e.formData)} - onSubmit={(e) => handleFormSubmit(type.id, e.formData)} readonly={!isEditMode} className={isEditMode ? "" : "pointer-events-none opacity-80"} > - {isEditMode ? ( -
- -
- ) : <>} + {/* No submit button — changes are saved via the global save (Ctrl+S) */} + <> diff --git a/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx b/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx index 21734064..b5ee98b9 100644 --- a/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx +++ b/packages/ui/src/pages/Post/renderers/CompactRenderer.tsx @@ -71,7 +71,7 @@ export const CompactRenderer: React.FC = (props) => {
{/* Left Column - Media */} -
+
{/* Desktop Gallery - Combines Media Viewer + Filmstrip */}
diff --git a/packages/ui/src/pages/SearchResults.tsx b/packages/ui/src/pages/SearchResults.tsx index f12c27c4..58da2da2 100644 --- a/packages/ui/src/pages/SearchResults.tsx +++ b/packages/ui/src/pages/SearchResults.tsx @@ -1,9 +1,10 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useSearchParams, useNavigate } from "react-router-dom"; import { useAuth } from "@/hooks/useAuth"; import { usePostNavigation } from "@/contexts/PostNavigationContext"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { ArrowLeft, Search, Image as ImageIcon, FileText, MessageSquare } from "lucide-react"; import { ThemeToggle } from "@/components/ThemeToggle"; import MediaCard from "@/components/MediaCard"; @@ -24,9 +25,38 @@ const SearchResults = () => { const [userLikes, setUserLikes] = useState>(new Set()); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState('all'); + const [inputQuery, setInputQuery] = useState(''); + const searchInputRef = useRef(null); const query = searchParams.get('q') || ''; + // Sync input with URL query + useEffect(() => { + setInputQuery(query); + }, [query]); + + // Auto-focus on mount when no query + useEffect(() => { + if (!query.trim()) { + searchInputRef.current?.focus(); + } + }, []); + + const handleSearchSubmit = (e: React.FormEvent) => { + e.preventDefault(); + if (inputQuery.trim()) { + navigate(`/search?q=${encodeURIComponent(inputQuery.trim())}`); + searchInputRef.current?.blur(); + } + }; + + const handleSearchKeyDown = (e: React.KeyboardEvent) => { + if (e.key === 'Escape') { + setInputQuery(''); + searchInputRef.current?.blur(); + } + }; + useEffect(() => { if (query.trim()) { performSearch(); @@ -184,7 +214,21 @@ const SearchResults = () => { return (
-
+
+
+ + setInputQuery(e.target.value)} + onKeyDown={handleSearchKeyDown} + /> + +
+

Enter a search term

Search for pages, posts, and pictures

@@ -198,21 +242,33 @@ const SearchResults = () => {
{/* Header */} -
+
- {/* Search Header */} -
-
- -

Search results for "{query}"

-
-

- {feedPosts.length} {feedPosts.length === 1 ? 'result' : 'results'} found + {/* Search Input */} +

+
+ + setInputQuery(e.target.value)} + onKeyDown={handleSearchKeyDown} + /> + +
+ + {/* Results Summary */} +
+

+ {feedPosts.length} {feedPosts.length === 1 ? 'result' : 'results'} for "{query}"