mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-05-05 22:02:39 +02:00
Merge pull request #1117 from sukarxn/feature/replace-raw-img-tags-with-next/image
fix: optimize image components with next/image
This commit is contained in:
commit
f35f5cef09
5 changed files with 160 additions and 75 deletions
|
|
@ -6,6 +6,7 @@ import { ImageIcon, ImageOffIcon } from "lucide-react";
|
||||||
import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
|
import { memo, type PropsWithChildren, useEffect, useRef, useState } from "react";
|
||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import NextImage from 'next/image';
|
||||||
|
|
||||||
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
|
const imageVariants = cva("aui-image-root relative overflow-hidden rounded-lg", {
|
||||||
variants: {
|
variants: {
|
||||||
|
|
@ -86,8 +87,8 @@ function ImagePreview({
|
||||||
>
|
>
|
||||||
<ImageOffIcon className="size-8 text-muted-foreground" />
|
<ImageOffIcon className="size-8 text-muted-foreground" />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : isDataOrBlobUrl(src) ? (
|
||||||
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
|
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||||
<img
|
<img
|
||||||
ref={imgRef}
|
ref={imgRef}
|
||||||
src={src}
|
src={src}
|
||||||
|
|
@ -103,6 +104,40 @@ function ImagePreview({
|
||||||
}}
|
}}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
// biome-ignore lint/performance/noImgElement: intentional for dynamic external URLs
|
||||||
|
// <img
|
||||||
|
// ref={imgRef}
|
||||||
|
// src={src}
|
||||||
|
// alt={alt}
|
||||||
|
// className={cn("block h-auto w-full object-contain", !loaded && "invisible", className)}
|
||||||
|
// onLoad={(e) => {
|
||||||
|
// if (typeof src === "string") setLoadedSrc(src);
|
||||||
|
// onLoad?.(e);
|
||||||
|
// }}
|
||||||
|
// onError={(e) => {
|
||||||
|
// if (typeof src === "string") setErrorSrc(src);
|
||||||
|
// onError?.(e);
|
||||||
|
// }}
|
||||||
|
// {...props}
|
||||||
|
// />
|
||||||
|
<NextImage
|
||||||
|
fill
|
||||||
|
src={src || ""}
|
||||||
|
alt={alt}
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 80vw, 60vw"
|
||||||
|
className={cn("block object-contain", !loaded && "invisible", className)}
|
||||||
|
onLoad={() => {
|
||||||
|
if (typeof src === "string") setLoadedSrc(src);
|
||||||
|
onLoad?.();
|
||||||
|
}}
|
||||||
|
onError={() => {
|
||||||
|
if (typeof src === "string") setErrorSrc(src);
|
||||||
|
onError?.();
|
||||||
|
}}
|
||||||
|
unoptimized={false}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -126,7 +161,10 @@ type ImageZoomProps = PropsWithChildren<{
|
||||||
src: string;
|
src: string;
|
||||||
alt?: string;
|
alt?: string;
|
||||||
}>;
|
}>;
|
||||||
|
function isDataOrBlobUrl(src: string | undefined): boolean {
|
||||||
|
if (!src || typeof src !== "string") return false;
|
||||||
|
return src.startsWith("data:") || src.startsWith("blob:");
|
||||||
|
}
|
||||||
function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
|
@ -177,6 +215,8 @@ function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
||||||
aria-label="Close zoomed image"
|
aria-label="Close zoomed image"
|
||||||
>
|
>
|
||||||
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
|
{/** biome-ignore lint/performance/noImgElement: <explanation> */}
|
||||||
|
{isDataOrBlobUrl(src) ? (
|
||||||
|
// biome-ignore lint/performance/noImgElement: data/blob URLs need plain img
|
||||||
<img
|
<img
|
||||||
data-slot="image-zoom-content"
|
data-slot="image-zoom-content"
|
||||||
src={src}
|
src={src}
|
||||||
|
|
@ -193,6 +233,21 @@ function ImageZoom({ src, alt = "Image preview", children }: ImageZoomProps) {
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<NextImage
|
||||||
|
data-slot="image-zoom-content"
|
||||||
|
fill
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
sizes="90vw"
|
||||||
|
className="aui-image-zoom-content fade-in zoom-in-95 object-contain duration-200"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
handleClose();
|
||||||
|
}}
|
||||||
|
unoptimized={false}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</button>,
|
</button>,
|
||||||
document.body
|
document.body
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
import Image from 'next/image';
|
||||||
|
|
||||||
import { AnimatePresence, motion } from "motion/react";
|
import { AnimatePresence, motion } from "motion/react";
|
||||||
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
|
import { ExpandedGifOverlay, useExpandedGif } from "@/components/ui/expanded-gif-overlay";
|
||||||
|
|
@ -81,6 +82,15 @@ function UseCaseCard({
|
||||||
alt={title}
|
alt={title}
|
||||||
className="w-full rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
className="w-full rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
||||||
/>
|
/>
|
||||||
|
<div className="relative w-full h-48">
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={title}
|
||||||
|
fill
|
||||||
|
className="rounded-xl object-cover transition-transform duration-500 group-hover:scale-[1.02]"
|
||||||
|
unoptimized={src.endsWith('.gif')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-5 py-4">
|
<div className="px-5 py-4">
|
||||||
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">{title}</h3>
|
<h3 className="text-base font-semibold text-neutral-900 dark:text-white">{title}</h3>
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@ import { createMathPlugin } from "@streamdown/math";
|
||||||
import { Streamdown, type StreamdownProps } from "streamdown";
|
import { Streamdown, type StreamdownProps } from "streamdown";
|
||||||
import "katex/dist/katex.min.css";
|
import "katex/dist/katex.min.css";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import Image from 'next/image';
|
||||||
|
import { is } from "drizzle-orm";
|
||||||
|
|
||||||
const code = createCodePlugin({
|
const code = createCodePlugin({
|
||||||
themes: ["nord", "nord"],
|
themes: ["nord", "nord"],
|
||||||
|
|
@ -127,16 +129,31 @@ export function MarkdownViewer({ content, className, maxLength }: MarkdownViewer
|
||||||
<blockquote className="border-l-4 border-muted pl-4 italic my-2" {...props} />
|
<blockquote className="border-l-4 border-muted pl-4 italic my-2" {...props} />
|
||||||
),
|
),
|
||||||
hr: ({ ...props }) => <hr className="my-4 border-muted" {...props} />,
|
hr: ({ ...props }) => <hr className="my-4 border-muted" {...props} />,
|
||||||
img: ({ src, alt, width: _w, height: _h, ...props }) => (
|
img: ({ src, alt, width: _w, height: _h, ...props }) => {
|
||||||
|
const isDataOrUnknownUrl = typeof src === "string" && (src.startsWith("data:") || !src.startsWith("http"));
|
||||||
|
|
||||||
|
return isDataOrUnknownUrl ? (
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
// eslint-disable-next-line @next/next/no-img-element
|
||||||
<img
|
<img
|
||||||
className="max-w-full h-auto my-4 rounded"
|
className="max-w-full h-auto my-4 rounded"
|
||||||
alt={alt || "markdown image"}
|
alt={alt || "markdown image"}
|
||||||
src={typeof src === "string" ? src : ""}
|
src={src}
|
||||||
loading="lazy"
|
loading="lazy"
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
),
|
) : (
|
||||||
|
<Image
|
||||||
|
className="max-w-full h-auto my-4 rounded"
|
||||||
|
alt={alt || "markdown image"}
|
||||||
|
src={typeof src === "string" ? src : ""}
|
||||||
|
width={_w || 800}
|
||||||
|
height={_h || 600}
|
||||||
|
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 75vw, 60vw"
|
||||||
|
unoptimized={isDataOrUnknownUrl}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
table: ({ ...props }) => (
|
table: ({ ...props }) => (
|
||||||
<div className="overflow-x-auto my-4 rounded-lg border border-border w-full">
|
<div className="overflow-x-auto my-4 rounded-lg border border-border w-full">
|
||||||
<table className="w-full divide-y divide-border" {...props} />
|
<table className="w-full divide-y divide-border" {...props} />
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ import { openSafeNavigationHref, resolveSafeNavigationHref } from "../shared/med
|
||||||
import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
||||||
import { Citation } from "./citation";
|
import { Citation } from "./citation";
|
||||||
import type { CitationType, CitationVariant, SerializableCitation } from "./schema";
|
import type { CitationType, CitationVariant, SerializableCitation } from "./schema";
|
||||||
|
import NextImage from 'next/image';
|
||||||
|
|
||||||
|
|
||||||
const TYPE_ICONS: Record<CitationType, LucideIcon> = {
|
const TYPE_ICONS: Record<CitationType, LucideIcon> = {
|
||||||
webpage: Globe,
|
webpage: Globe,
|
||||||
|
|
@ -253,17 +255,17 @@ function OverflowItem({ citation, onClick }: OverflowItemProps) {
|
||||||
className="group hover:bg-muted focus-visible:bg-muted flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-left transition-colors focus-visible:outline-none"
|
className="group hover:bg-muted focus-visible:bg-muted flex w-full cursor-pointer items-center gap-2.5 rounded-md px-2 py-2 text-left transition-colors focus-visible:outline-none"
|
||||||
>
|
>
|
||||||
{citation.favicon ? (
|
{citation.favicon ? (
|
||||||
// biome-ignore lint/performance/noImgElement: external favicon from arbitrary domain — next/image requires remotePatterns config
|
<NextImage
|
||||||
<img
|
|
||||||
src={citation.favicon}
|
src={citation.favicon}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
width={16}
|
width={18}
|
||||||
height={16}
|
height={18}
|
||||||
className="bg-muted size-4 shrink-0 rounded object-cover"
|
className="size-4.5 rounded-full object-cover"
|
||||||
|
unoptimized={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TypeIcon className="text-muted-foreground size-4 shrink-0" aria-hidden="true" />
|
<TypeIcon className="text-muted-foreground size-3" aria-hidden="true" />
|
||||||
)}
|
)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="group-hover:decoration-foreground/30 truncate text-sm font-medium group-hover:underline group-hover:underline-offset-2">
|
<p className="group-hover:decoration-foreground/30 truncate text-sm font-medium group-hover:underline group-hover:underline-offset-2">
|
||||||
|
|
@ -339,14 +341,14 @@ function StackedCitations({ id, citations, className, onNavigate }: StackedCitat
|
||||||
style={{ zIndex: maxIcons - index }}
|
style={{ zIndex: maxIcons - index }}
|
||||||
>
|
>
|
||||||
{citation.favicon ? (
|
{citation.favicon ? (
|
||||||
// biome-ignore lint/performance/noImgElement: external favicon from arbitrary domain — next/image requires remotePatterns config
|
<NextImage
|
||||||
<img
|
|
||||||
src={citation.favicon}
|
src={citation.favicon}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
width={18}
|
width={18}
|
||||||
height={18}
|
height={18}
|
||||||
className="size-4.5 rounded-full object-cover"
|
className="size-4.5 rounded-full object-cover"
|
||||||
|
unoptimized={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TypeIcon className="text-muted-foreground size-3" aria-hidden="true" />
|
<TypeIcon className="text-muted-foreground size-3" aria-hidden="true" />
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import * as React from "react";
|
||||||
import { openSafeNavigationHref, sanitizeHref } from "../shared/media";
|
import { openSafeNavigationHref, sanitizeHref } from "../shared/media";
|
||||||
import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
import { cn, Popover, PopoverContent, PopoverTrigger } from "./_adapter";
|
||||||
import type { CitationType, CitationVariant, SerializableCitation } from "./schema";
|
import type { CitationType, CitationVariant, SerializableCitation } from "./schema";
|
||||||
|
import NextImage from 'next/image';
|
||||||
|
|
||||||
const FALLBACK_LOCALE = "en-US";
|
const FALLBACK_LOCALE = "en-US";
|
||||||
|
|
||||||
|
|
@ -114,18 +115,18 @@ export function Citation(props: CitationProps) {
|
||||||
};
|
};
|
||||||
|
|
||||||
const iconElement = favicon ? (
|
const iconElement = favicon ? (
|
||||||
// biome-ignore lint/performance/noImgElement: external favicon from arbitrary domain — next/image requires remotePatterns config
|
<NextImage
|
||||||
<img
|
|
||||||
src={favicon}
|
src={favicon}
|
||||||
alt=""
|
alt=""
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
width={14}
|
width={16}
|
||||||
height={14}
|
height={16}
|
||||||
className="bg-muted size-3.5 shrink-0 rounded object-cover"
|
className="bg-muted size-3.5 shrink-0 rounded object-cover"
|
||||||
|
unoptimized={true}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<TypeIcon className="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
|
<TypeIcon className="size-3.5 shrink-0 opacity-60" aria-hidden="true" />
|
||||||
);
|
);
|
||||||
|
|
||||||
const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();
|
const { open, handleMouseEnter, handleMouseLeave } = useHoverPopover();
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue