ui shit
This commit is contained in:
parent
bac7d0979f
commit
362e0fe83c
@ -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
|
||||
);
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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 */}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 };
|
||||
};
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user