This commit is contained in:
lovebird 2026-02-19 13:24:04 +01:00
parent bac7d0979f
commit 362e0fe83c
8 changed files with 136 additions and 47 deletions

View File

@ -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(
<div
className="fixed inset-0 bg-black/95 z-[9999] flex items-center justify-center"
onClick={(e) => {
@ -320,8 +318,8 @@ export default function ImageLightbox({
}
}}
>
<div className="relative w-full h-full flex items-center justify-center">
<div className="relative w-full h-full flex items-center justify-center">
<div className="relative w-full h-full flex items-center justify-center min-w-0 min-h-0 overflow-hidden">
<div className="relative w-full h-full flex items-center justify-center min-w-0 min-h-0">
{isMobile ? responsiveImageEl : (
<TransformWrapper
initialScale={1}
@ -895,6 +893,7 @@ export default function ImageLightbox({
originalImageId={originalImageId}
isPublishing={isPublishing}
/>
</div >
</div >,
document.body
);
}

View File

@ -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}`}
>
<ReactMarkdown
remarkPlugins={[remarkGfm]}
components={{
img: ({ node, src, alt, title, ...props }) => {
// Basic implementation of ResponsiveImage
@ -245,6 +247,20 @@ const MarkdownRenderer = React.memo(({ content, className = "", variables }: Mar
}
return <p {...props}>{children}</p>;
},
table: ({ node, ...props }) => (
<div className="overflow-x-auto my-4">
<table className="min-w-full border-collapse border border-border" {...props} />
</div>
),
thead: ({ node, ...props }) => (
<thead className="bg-muted/50" {...props} />
),
th: ({ node, ...props }) => (
<th className="border border-border px-3 py-2 text-left text-sm font-semibold" {...props} />
),
td: ({ node, ...props }) => (
<td className="border border-border px-3 py-2 text-sm" {...props} />
),
}}
>
{finalContent}

View File

@ -73,7 +73,7 @@ const PhotoCard = ({
apiUrl,
versionCount,
isExternal = false,
imageFit = 'cover',
imageFit = 'contain',
className,
preset
}: PhotoCardProps) => {
@ -335,7 +335,7 @@ const PhotoCard = ({
return (
<div
data-testid="photo-card"
className={`group relative overflow-hidden bg-card transition-all duration-300 cursor-pointer w-full ${className || ''}`}
className={`group w-full relative overflow-hidden bg-card transition-all duration-300 cursor-pointer w-full ${className || ''}`}
onClick={handleCardClick}
>
{/* Image */}

View File

@ -29,13 +29,16 @@ export interface ResponsiveData {
stats?: any;
}
const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
const DEFAULT_RESPONSIVE_SIZES = [640, 1280];
const DEFAULT_FORMATS = ['avif'];
const ResponsiveImage: React.FC<ResponsiveImageProps> = 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<ResponsiveImageProps> = ({
useEffect(() => {
if (data && onDataLoaded) {
console.log('onDataLoaded data ', data)
onDataLoaded(data);
}
}, [data, onDataLoaded]);
@ -100,10 +104,14 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
// 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<ResponsiveImageProps> = ({
}
return (
<div className={`relative w-full h-full ${className || ''}`}>
<div className={`relative w-full h-full overflow-hidden ${className || ''}`}>
<picture>
{(data.sources || []).map((source, index) => (
<source key={index} srcSet={source.srcset} type={source.type} sizes={sizes} />
@ -158,6 +166,17 @@ const ResponsiveImage: React.FC<ResponsiveImageProps> = ({
)}
</div>
);
};
}, (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;

View File

@ -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<ResponsiveData>
const requestCache = new Map<string, Promise<ResponsiveData>>();
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<string | null>(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 };
};

View File

@ -51,14 +51,13 @@ export const UserPageTypeFields: React.FC<UserPageTypeFieldsProps> = ({
}
}, [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<UserPageTypeFieldsProps> = ({
{ 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<UserPageTypeFieldsProps> = ({
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 ? (
<div className="flex justify-end mt-4">
<button type="submit" className="bg-primary text-primary-foreground hover:bg-primary/90 px-4 py-2 rounded-md text-xs font-medium transition-colors">
Save {type.name} Values
</button>
</div>
) : <></>}
{/* No submit button — changes are saved via the global save (Ctrl+S) */}
<></>
</Form>
</AccordionContent>
</AccordionItem>

View File

@ -71,7 +71,7 @@ export const CompactRenderer: React.FC<PostRendererProps> = (props) => {
<div className="overflow-hidden-x group h-[inherit]">
<div className="grid grid-cols-1 lg:grid-cols-2 h-[inherit]">
{/* Left Column - Media */}
<div className={`${isVideo ? 'aspect-video' : 'aspect-square'} lg:aspect-auto bg-background border flex flex-col relative h-full`}>
<div className={`${isVideo ? 'aspect-video' : 'aspect-square'} lg:aspect-auto bg-background border flex flex-col relative h-full w-full`}>
{/* Desktop Gallery - Combines Media Viewer + Filmstrip */}
<div className="hidden lg:block h-full">

View File

@ -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<Set<string>>(new Set());
const [loading, setLoading] = useState(true);
const [activeTab, setActiveTab] = useState<SearchTab>('all');
const [inputQuery, setInputQuery] = useState('');
const searchInputRef = useRef<HTMLInputElement>(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 (
<div className="min-h-screen bg-background pt-14">
<div className="container mx-auto px-4 py-8 max-w-6xl">
<div className="text-center py-16">
<div className="max-w-md mx-auto mt-8 mb-8">
<form onSubmit={handleSearchSubmit} className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={searchInputRef}
type="search"
placeholder="Search pages, posts, and pictures..."
className="pl-10 pr-4 h-11 w-full bg-muted/50 border focus-visible:ring-1 focus-visible:ring-primary rounded-xl"
value={inputQuery}
onChange={(e) => setInputQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
/>
</form>
</div>
<div className="text-center py-12">
<Search className="h-16 w-16 mx-auto mb-4 text-muted-foreground" />
<h3 className="text-xl font-semibold mb-2">Enter a search term</h3>
<p className="text-muted-foreground">Search for pages, posts, and pictures</p>
@ -198,21 +242,33 @@ const SearchResults = () => {
<div className="min-h-screen bg-background pt-14">
<div className="container mx-auto px-4 py-8 max-w-6xl">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div className="flex items-center justify-between mb-4">
<Button variant="ghost" size="sm" onClick={() => navigate(-1)}>
<ArrowLeft className="h-4 w-4 mr-2" /> Back
</Button>
<ThemeToggle />
</div>
{/* Search Header */}
<div className="mb-8">
<div className="flex items-center gap-2 mb-4">
<Search className="h-6 w-6 text-primary" />
<h1 className="text-2xl font-bold">Search results for "{query}"</h1>
</div>
<p className="text-muted-foreground">
{feedPosts.length} {feedPosts.length === 1 ? 'result' : 'results'} found
{/* Search Input */}
<div className="max-w-md mx-auto mb-6">
<form onSubmit={handleSearchSubmit} className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
ref={searchInputRef}
type="search"
placeholder="Search pages, posts, and pictures..."
className="pl-10 pr-4 h-11 w-full bg-muted/50 border focus-visible:ring-1 focus-visible:ring-primary rounded-xl"
value={inputQuery}
onChange={(e) => setInputQuery(e.target.value)}
onKeyDown={handleSearchKeyDown}
/>
</form>
</div>
{/* Results Summary */}
<div className="mb-6">
<p className="text-muted-foreground text-center">
{feedPosts.length} {feedPosts.length === 1 ? 'result' : 'results'} for <strong>"{query}"</strong>
</p>
</div>