mirror of
https://github.com/Dokploy/website
synced 2025-06-26 18:16:01 +00:00
feat: add zoomable images to blog posts using react-photo-view
This commit is contained in:
parent
d03d30aa76
commit
7429b4995e
@ -0,0 +1,26 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import { PhotoProvider, PhotoView } from "react-photo-view";
|
||||||
|
import "react-photo-view/dist/react-photo-view.css";
|
||||||
|
|
||||||
|
interface ZoomableImageProps {
|
||||||
|
src: string;
|
||||||
|
alt: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ZoomableImage({ src, alt, className }: ZoomableImageProps) {
|
||||||
|
return (
|
||||||
|
<PhotoProvider>
|
||||||
|
<PhotoView src={src}>
|
||||||
|
<Image
|
||||||
|
src={src}
|
||||||
|
alt={alt}
|
||||||
|
fill
|
||||||
|
className={`object-cover cursor-zoom-in ${className || ""}`}
|
||||||
|
/>
|
||||||
|
</PhotoView>
|
||||||
|
</PhotoProvider>
|
||||||
|
);
|
||||||
|
}
|
@ -10,6 +10,7 @@ import ReactMarkdown from "react-markdown";
|
|||||||
import type { Components } from "react-markdown";
|
import type { Components } from "react-markdown";
|
||||||
import rehypeRaw from "rehype-raw";
|
import rehypeRaw from "rehype-raw";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
|
import { ZoomableImage } from "./components/ZoomableImage";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
params: { locale: string; slug: string };
|
params: { locale: string; slug: string };
|
||||||
@ -114,7 +115,7 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
),
|
),
|
||||||
img: ({ node, src, alt }) => (
|
img: ({ node, src, alt }) => (
|
||||||
<div className="relative w-full h-64 my-6 rounded-lg overflow-hidden">
|
<div className="relative w-full h-64 my-6 rounded-lg overflow-hidden">
|
||||||
<Image src={src || ""} alt={alt || ""} fill className="object-cover" />
|
{src && <ZoomableImage src={src} alt={alt || ""} />}
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
@ -164,13 +165,11 @@ export default async function BlogPostPage({ params }: Props) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{post.feature_image && (
|
{post.feature_image && (
|
||||||
<div className="relative h-96 w-full rounded-lg overflow-hidden mb-8">
|
<div className="relative w-full h-[400px] mb-8">
|
||||||
<Image
|
<ZoomableImage
|
||||||
src={post.feature_image}
|
src={post.feature_image}
|
||||||
alt={post.title}
|
alt={post.title}
|
||||||
fill
|
className="rounded-lg"
|
||||||
className="object-cover"
|
|
||||||
priority
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -73,7 +73,7 @@ export default async function BlogPage({
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
{filteredPosts.length === 0 ? (
|
{filteredPosts.length === 0 ? (
|
||||||
<div className="text-center py-12">
|
<div className="text-center py-12 min-h-[20vh] flex items-center justify-center">
|
||||||
<p className="text-xl text-muted-foreground">
|
<p className="text-xl text-muted-foreground">
|
||||||
{search || selectedTag ? t("noResults") : t("noPosts")}
|
{search || selectedTag ? t("noResults") : t("noPosts")}
|
||||||
</p>
|
</p>
|
||||||
|
@ -3,7 +3,7 @@ import { Inter, Lexend } from "next/font/google";
|
|||||||
import "@/styles/tailwind.css";
|
import "@/styles/tailwind.css";
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getMessages } from "next-intl/server";
|
import { getMessages } from "next-intl/server";
|
||||||
|
import "react-photo-view/dist/react-photo-view.css";
|
||||||
import { Footer } from "@/components/Footer";
|
import { Footer } from "@/components/Footer";
|
||||||
import { Header } from "@/components/Header";
|
import { Header } from "@/components/Header";
|
||||||
import type { Metadata } from "next";
|
import type { Metadata } from "next";
|
||||||
|
@ -1,6 +1,25 @@
|
|||||||
import { getPosts } from "@/lib/ghost";
|
import { getPosts } from "@/lib/ghost";
|
||||||
import { NextResponse } from "next/server";
|
import { NextResponse } from "next/server";
|
||||||
|
|
||||||
|
function escapeXml(unsafe: string): string {
|
||||||
|
return unsafe.replace(/[<>&'"]/g, (c) => {
|
||||||
|
switch (c) {
|
||||||
|
case "<":
|
||||||
|
return "<";
|
||||||
|
case ">":
|
||||||
|
return ">";
|
||||||
|
case "&":
|
||||||
|
return "&";
|
||||||
|
case "'":
|
||||||
|
return "'";
|
||||||
|
case '"':
|
||||||
|
return """;
|
||||||
|
default:
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
export async function GET() {
|
export async function GET() {
|
||||||
const posts = await getPosts();
|
const posts = await getPosts();
|
||||||
|
|
||||||
@ -17,13 +36,14 @@ export async function GET() {
|
|||||||
(post) => `
|
(post) => `
|
||||||
<item>
|
<item>
|
||||||
<title><![CDATA[${post.title}]]></title>
|
<title><![CDATA[${post.title}]]></title>
|
||||||
<link>https://dokploy.com/blog/${post.slug}</link>
|
<link>https://dokploy.com/blog/${escapeXml(post.slug)}</link>
|
||||||
<guid>https://dokploy.com/blog/${post.slug}</guid>
|
<guid>https://dokploy.com/blog/${escapeXml(post.slug)}</guid>
|
||||||
<description><![CDATA[${post.excerpt}]]></description>
|
<description><![CDATA[${post.excerpt}]]></description>
|
||||||
|
<content:encoded><![CDATA[${post.html}]]></content:encoded>
|
||||||
<pubDate>${new Date(post.published_at).toUTCString()}</pubDate>
|
<pubDate>${new Date(post.published_at).toUTCString()}</pubDate>
|
||||||
${
|
${
|
||||||
post.feature_image
|
post.feature_image
|
||||||
? `<enclosure url="${post.feature_image}" type="image/jpeg" />`
|
? `<enclosure url="${escapeXml(post.feature_image)}" type="image/jpeg" />`
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
${
|
${
|
||||||
@ -39,7 +59,7 @@ export async function GET() {
|
|||||||
|
|
||||||
return new NextResponse(rss, {
|
return new NextResponse(rss, {
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/xml",
|
"Content-Type": "application/xml; charset=utf-8",
|
||||||
"Cache-Control": "s-maxage=3600, stale-while-revalidate",
|
"Cache-Control": "s-maxage=3600, stale-while-revalidate",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
},
|
},
|
||||||
"browserslist": "defaults, not ie <= 11",
|
"browserslist": "defaults, not ie <= 11",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"react-photo-view": "^1.2.7",
|
||||||
"@headlessui/react": "^1.7.17",
|
"@headlessui/react": "^1.7.17",
|
||||||
"@headlessui/tailwindcss": "^0.2.0",
|
"@headlessui/tailwindcss": "^0.2.0",
|
||||||
"@radix-ui/react-accordion": "^1.2.1",
|
"@radix-ui/react-accordion": "^1.2.1",
|
||||||
|
@ -173,6 +173,9 @@ importers:
|
|||||||
react-ga4:
|
react-ga4:
|
||||||
specifier: ^2.1.0
|
specifier: ^2.1.0
|
||||||
version: 2.1.0
|
version: 2.1.0
|
||||||
|
react-photo-view:
|
||||||
|
specifier: ^1.2.7
|
||||||
|
version: 1.2.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
tailwind-merge:
|
tailwind-merge:
|
||||||
specifier: ^2.2.2
|
specifier: ^2.2.2
|
||||||
version: 2.4.0
|
version: 2.4.0
|
||||||
@ -3025,6 +3028,12 @@ packages:
|
|||||||
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0
|
||||||
|
|
||||||
|
react-photo-view@1.2.7:
|
||||||
|
resolution: {integrity: sha512-MfOWVPxuibncRLaycZUNxqYU8D9IA+rbGDDaq6GM8RIoGJal592hEJoRAyRSI7ZxyyJNJTLMUWWL3UIXHJJOpw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.8.0'
|
||||||
|
react-dom: '>=16.8.0'
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.6:
|
react-remove-scroll-bar@2.3.6:
|
||||||
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
|
||||||
engines: {node: '>=10'}
|
engines: {node: '>=10'}
|
||||||
@ -6811,6 +6820,11 @@ snapshots:
|
|||||||
react: 18.3.1
|
react: 18.3.1
|
||||||
react-dom: 18.3.1(react@18.3.1)
|
react-dom: 18.3.1(react@18.3.1)
|
||||||
|
|
||||||
|
react-photo-view@1.2.7(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
|
||||||
react-remove-scroll-bar@2.3.6(@types/react@18.3.5)(react@18.2.0):
|
react-remove-scroll-bar@2.3.6(@types/react@18.3.5)(react@18.2.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
|
Loading…
Reference in New Issue
Block a user