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 ( + + + {alt} + + + ); +} 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 }) => (
- {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) => ` <![CDATA[${post.title}]]> - 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