From 7429b4995e05aa4404a1d8d0059deaa57cbc1940 Mon Sep 17 00:00:00 2001
From: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Date: Fri, 28 Feb 2025 01:28:45 -0600
Subject: [PATCH] feat: add zoomable images to blog posts using
react-photo-view
---
.../blog/[slug]/components/ZoomableImage.tsx | 26 +++++++++++++++++
.../website/app/[locale]/blog/[slug]/page.tsx | 11 ++++----
apps/website/app/[locale]/blog/page.tsx | 2 +-
apps/website/app/[locale]/layout.tsx | 2 +-
apps/website/app/rss.xml/route.ts | 28 ++++++++++++++++---
apps/website/package.json | 1 +
pnpm-lock.yaml | 14 ++++++++++
7 files changed, 72 insertions(+), 12 deletions(-)
create mode 100644 apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx
diff --git a/apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx b/apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx
new file mode 100644
index 0000000..dd0c276
--- /dev/null
+++ b/apps/website/app/[locale]/blog/[slug]/components/ZoomableImage.tsx
@@ -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 (
+
+
+
+
+
+ );
+}
diff --git a/apps/website/app/[locale]/blog/[slug]/page.tsx b/apps/website/app/[locale]/blog/[slug]/page.tsx
index 2bbef31..93589ec 100644
--- a/apps/website/app/[locale]/blog/[slug]/page.tsx
+++ b/apps/website/app/[locale]/blog/[slug]/page.tsx
@@ -10,6 +10,7 @@ import ReactMarkdown from "react-markdown";
import type { Components } from "react-markdown";
import rehypeRaw from "rehype-raw";
import remarkGfm from "remark-gfm";
+import { ZoomableImage } from "./components/ZoomableImage";
type Props = {
params: { locale: string; slug: string };
@@ -114,7 +115,7 @@ export default async function BlogPostPage({ params }: Props) {
),
img: ({ node, src, alt }) => (
-
+ {src && }
),
};
@@ -164,13 +165,11 @@ export default async function BlogPostPage({ params }: Props) {
{post.feature_image && (
-
-
+
)}
diff --git a/apps/website/app/[locale]/blog/page.tsx b/apps/website/app/[locale]/blog/page.tsx
index 57206e6..7c5d825 100644
--- a/apps/website/app/[locale]/blog/page.tsx
+++ b/apps/website/app/[locale]/blog/page.tsx
@@ -73,7 +73,7 @@ export default async function BlogPage({
/>
{filteredPosts.length === 0 ? (
-
+
{search || selectedTag ? t("noResults") : t("noPosts")}
diff --git a/apps/website/app/[locale]/layout.tsx b/apps/website/app/[locale]/layout.tsx
index 8663c75..e6fee90 100644
--- a/apps/website/app/[locale]/layout.tsx
+++ b/apps/website/app/[locale]/layout.tsx
@@ -3,7 +3,7 @@ import { Inter, Lexend } from "next/font/google";
import "@/styles/tailwind.css";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
-
+import "react-photo-view/dist/react-photo-view.css";
import { Footer } from "@/components/Footer";
import { Header } from "@/components/Header";
import type { Metadata } from "next";
diff --git a/apps/website/app/rss.xml/route.ts b/apps/website/app/rss.xml/route.ts
index be9041c..f68b008 100644
--- a/apps/website/app/rss.xml/route.ts
+++ b/apps/website/app/rss.xml/route.ts
@@ -1,6 +1,25 @@
import { getPosts } from "@/lib/ghost";
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() {
const posts = await getPosts();
@@ -17,13 +36,14 @@ export async function GET() {
(post) => `
-
- https://dokploy.com/blog/${post.slug}
- https://dokploy.com/blog/${post.slug}
+ https://dokploy.com/blog/${escapeXml(post.slug)}
+ https://dokploy.com/blog/${escapeXml(post.slug)}
+
${new Date(post.published_at).toUTCString()}
${
post.feature_image
- ? ``
+ ? ``
: ""
}
${
@@ -39,7 +59,7 @@ export async function GET() {
return new NextResponse(rss, {
headers: {
- "Content-Type": "application/xml",
+ "Content-Type": "application/xml; charset=utf-8",
"Cache-Control": "s-maxage=3600, stale-while-revalidate",
},
});
diff --git a/apps/website/package.json b/apps/website/package.json
index 700dc99..0a5992d 100644
--- a/apps/website/package.json
+++ b/apps/website/package.json
@@ -12,6 +12,7 @@
},
"browserslist": "defaults, not ie <= 11",
"dependencies": {
+ "react-photo-view": "^1.2.7",
"@headlessui/react": "^1.7.17",
"@headlessui/tailwindcss": "^0.2.0",
"@radix-ui/react-accordion": "^1.2.1",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 4c68e73..19b84df 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -173,6 +173,9 @@ importers:
react-ga4:
specifier: ^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:
specifier: ^2.2.2
version: 2.4.0
@@ -3025,6 +3028,12 @@ packages:
react: ^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:
resolution: {integrity: sha512-DtSYaao4mBmX+HDo5YWYdBWQwYIQQshUV/dVxFxK+KM26Wjwp1gZ6rv6OC3oujI6Bfu6Xyg3TwK533AQutsn/g==}
engines: {node: '>=10'}
@@ -6811,6 +6820,11 @@ snapshots:
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):
dependencies:
react: 18.2.0