From 43c6bceb94462c55893d750b1b422cdef7d0d20e Mon Sep 17 00:00:00 2001 From: Carlo Bortolan <106114526+carlobortolan@users.noreply.github.com> Date: Mon, 12 Jan 2026 02:11:27 +0900 Subject: [PATCH] QR page sharing (#368) * Add QR to ShareModal and update next image hostnames * Remove unused import * Add qrcode into socials and update i18n * Update README to include QR code option to social sharing * Include qrcode in AdminAppConfigurationClient --- README.md | 3 +- next.config.ts | 9 ++- .../config/AdminAppConfigurationClient.tsx | 6 +- src/i18n/locales/bd-bn.ts | 1 + src/i18n/locales/en-gb.ts | 1 + src/i18n/locales/en-us.ts | 1 + src/i18n/locales/hi-in.ts | 1 + src/i18n/locales/id-id.ts | 1 + src/i18n/locales/pt-br.ts | 1 + src/i18n/locales/pt-pt.ts | 1 + src/i18n/locales/tr-tr.ts | 1 + src/i18n/locales/zh-cn.ts | 1 + src/share/ShareModal.tsx | 56 ++++++++++++++----- src/social/SocialButton.tsx | 2 + src/social/index.ts | 8 +++ 15 files changed, 75 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3ee598ce..67de898d 100644 --- a/README.md +++ b/README.md @@ -178,12 +178,13 @@ Create Upstash Redis store from storage tab of Vercel dashboard and link to your - `NEXT_PUBLIC_GEO_PRIVACY = 1` disables collection/display of location-based data (⚠️ re-compresses uploaded images in order to remove GPS information) - `NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS = 1` enables public photo downloads for all visitors (⚠️ may result in increased bandwidth usage) - `NEXT_PUBLIC_SOCIAL_NETWORKS` - - Comma-separated list of social networks to show in share modal + - Comma-separated list of social networks and sharing options to show in share modal - Accepted values: - `x` (default) - `threads` - `facebook` - `linkedin` + - `qrcode` - `all` - `none` - `NEXT_PUBLIC_SITE_FEEDS = 1` enables feeds at `/feed.json` and `/rss.xml` diff --git a/next.config.ts b/next.config.ts index afd9e7cb..584e902f 100644 --- a/next.config.ts +++ b/next.config.ts @@ -39,7 +39,14 @@ const generateRemotePattern = ( pathname: '/**', }); -const remotePatterns: RemotePattern[] = []; +const remotePatterns: RemotePattern[] = [ + { + protocol: 'https', + hostname: 'api.qrserver.com', + port: '', + pathname: '/v1/create-qr-code/**', + }, +]; if (HOSTNAME_VERCEL_BLOB) { remotePatterns.push(generateRemotePattern(HOSTNAME_VERCEL_BLOB)); diff --git a/src/admin/config/AdminAppConfigurationClient.tsx b/src/admin/config/AdminAppConfigurationClient.tsx index a74fc179..0baa8289 100644 --- a/src/admin/config/AdminAppConfigurationClient.tsx +++ b/src/admin/config/AdminAppConfigurationClient.tsx @@ -912,14 +912,14 @@ export default function AdminAppConfigurationClient({ {renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])} {renderOrderedKeyList(socialKeys, SOCIAL_KEYS)}
- Configure order and visibility of social networks - (seen in share modal) by storing comma-separated values + Configure order and visibility of social networks and sharing + options (seen in share modal) by storing comma-separated values (accepts {'"all"'} or {'"none"'}, defaults to {renderCommaSeparatedList(DEFAULT_SOCIAL_KEYS)})
diff --git a/src/i18n/locales/bd-bn.ts b/src/i18n/locales/bd-bn.ts index 033b9749..75a1f1af 100644 --- a/src/i18n/locales/bd-bn.ts +++ b/src/i18n/locales/bd-bn.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Threads এ শেয়ার করুন', shareFacebook: 'Facebook এ শেয়ার করুন', shareLinkedIn: 'LinkedIn এ শেয়ার করুন', + shareQRCode: 'QR কোড টগল করুন', }, theme: { theme: 'থিম', diff --git a/src/i18n/locales/en-gb.ts b/src/i18n/locales/en-gb.ts index 83e43b40..8d759952 100644 --- a/src/i18n/locales/en-gb.ts +++ b/src/i18n/locales/en-gb.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Share on Threads', shareFacebook: 'Share on Facebook', shareLinkedIn: 'Share on LinkedIn', + shareQRCode: 'Toggle QR Code', }, theme: { theme: 'Theme', diff --git a/src/i18n/locales/en-us.ts b/src/i18n/locales/en-us.ts index b204d945..ddd2d4b4 100644 --- a/src/i18n/locales/en-us.ts +++ b/src/i18n/locales/en-us.ts @@ -95,6 +95,7 @@ export const TEXT = { shareThreads: 'Share on Threads', shareFacebook: 'Share on Facebook', shareLinkedIn: 'Share on LinkedIn', + shareQRCode: 'Toggle QR Code', }, theme: { theme: 'Theme', diff --git a/src/i18n/locales/hi-in.ts b/src/i18n/locales/hi-in.ts index 8d0b670b..8e429c88 100644 --- a/src/i18n/locales/hi-in.ts +++ b/src/i18n/locales/hi-in.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Threads पर साझा करें', shareFacebook: 'Facebook पर साझा करें', shareLinkedIn: 'LinkedIn पर साझा करें', + shareQRCode: 'QR कोड टॉगल करें', }, theme: { theme: 'थीम', diff --git a/src/i18n/locales/id-id.ts b/src/i18n/locales/id-id.ts index ca6284fa..79a55770 100644 --- a/src/i18n/locales/id-id.ts +++ b/src/i18n/locales/id-id.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Bagikan di Threads', shareFacebook: 'Bagikan di Facebook', shareLinkedIn: 'Bagikan di LinkedIn', + shareQRCode: 'Alihkan Kode QR', }, theme: { theme: 'Tema', diff --git a/src/i18n/locales/pt-br.ts b/src/i18n/locales/pt-br.ts index 35e1156e..356cb520 100644 --- a/src/i18n/locales/pt-br.ts +++ b/src/i18n/locales/pt-br.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Compartilhar no Threads', shareFacebook: 'Compartilhar no Facebook', shareLinkedIn: 'Compartilhar no LinkedIn', + shareQRCode: 'Alternar Código QR', }, theme: { theme: 'Tema', diff --git a/src/i18n/locales/pt-pt.ts b/src/i18n/locales/pt-pt.ts index 7b7da319..2cbd4e27 100644 --- a/src/i18n/locales/pt-pt.ts +++ b/src/i18n/locales/pt-pt.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Partilhar no Threads', shareFacebook: 'Partilhar no Facebook', shareLinkedIn: 'Partilhar no LinkedIn', + shareQRCode: 'Alternar Código QR', }, theme: { theme: 'Tema', diff --git a/src/i18n/locales/tr-tr.ts b/src/i18n/locales/tr-tr.ts index 5d6d84a8..b2ee323b 100644 --- a/src/i18n/locales/tr-tr.ts +++ b/src/i18n/locales/tr-tr.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: 'Threads\'te Paylaş', shareFacebook: 'Facebook\'ta Paylaş', shareLinkedIn: 'LinkedIn\'de Paylaş', + shareQRCode: 'QR Kodunu Göster/Gizle', }, theme: { theme: 'Tema', diff --git a/src/i18n/locales/zh-cn.ts b/src/i18n/locales/zh-cn.ts index b3bf488d..2f962402 100644 --- a/src/i18n/locales/zh-cn.ts +++ b/src/i18n/locales/zh-cn.ts @@ -96,6 +96,7 @@ export const TEXT: I18N = { shareThreads: '在 Threads 上分享', shareFacebook: '在 Facebook 上分享', shareLinkedIn: '在 LinkedIn 上分享', + shareQRCode: '切换二维码', }, theme: { theme: '主题', diff --git a/src/share/ShareModal.tsx b/src/share/ShareModal.tsx index 7d02f3ff..23896e85 100644 --- a/src/share/ShareModal.tsx +++ b/src/share/ShareModal.tsx @@ -1,10 +1,10 @@ 'use client'; import Modal from '@/components/Modal'; -import { TbPhotoShare } from 'react-icons/tb'; +import { TbPhotoShare, TbQrcode } from 'react-icons/tb'; import { clsx } from 'clsx/lite'; import { BiCopy } from 'react-icons/bi'; -import { ReactNode, useCallback, useEffect } from 'react'; +import { ReactNode, useCallback, useEffect, useState } from 'react'; import { shortenUrl } from '@/utility/url'; import { toastSuccess } from '@/toast'; import { SOCIAL_NETWORKS } from '@/app/config'; @@ -15,6 +15,7 @@ import MaskedScroll from '@/components/MaskedScroll'; import { useAppText } from '@/i18n/state/client'; import SocialButton from '@/social/SocialButton'; import LoaderButton from '@/components/primitives/LoaderButton'; +import Image from 'next/image'; const BUTTON_COLOR_CLASSNAMES = clsx( 'border-gray-200 bg-gray-50 active:bg-gray-100', @@ -43,6 +44,7 @@ export default function ShareModal({ } = useAppState(); const appText = useAppText(); + const [showQR, setShowQR] = useState(false); useEffect(() => { setShouldRespondToKeyboardCommands?.(false); @@ -90,7 +92,25 @@ export default function ShareModal({ {title} } - {children} + {!showQR ? ( + <>{children} + ) : ( +
+
+ QR Code +
+
+ )}
{SOCIAL_NETWORKS.map(key => - )} + key === 'qrcode' ? ( + renderButton( + , + () => setShowQR(q => !q), + false, + appText.tooltip.shareQRCode, + ) + ) : ( + + ), + )} {typeof navigator !== 'undefined' && navigator.share && renderButton( , diff --git a/src/social/SocialButton.tsx b/src/social/SocialButton.tsx index 8179b386..e1082ae0 100644 --- a/src/social/SocialButton.tsx +++ b/src/social/SocialButton.tsx @@ -5,6 +5,7 @@ import Link from 'next/link'; import clsx from 'clsx/lite'; import Tooltip from '@/components/Tooltip'; import { useAppText } from '@/i18n/state/client'; +import { TbQrcode } from 'react-icons/tb'; const iconForSocialKey = (key: SocialKey) => { switch (key) { @@ -12,6 +13,7 @@ const iconForSocialKey = (key: SocialKey) => { case 'threads': return ; case 'facebook': return ; case 'linkedin': return ; + case 'qrcode': return ; } }; diff --git a/src/social/index.ts b/src/social/index.ts index 5e512f8d..a563094e 100644 --- a/src/social/index.ts +++ b/src/social/index.ts @@ -6,6 +6,7 @@ export const SOCIAL_KEYS = [ 'threads', 'facebook', 'linkedin', + 'qrcode', ] as const; export type SocialKey = (typeof SOCIAL_KEYS)[number]; @@ -49,6 +50,12 @@ export const urlForSocial = ( url.searchParams.set('text', text); return url.toString(); } + case 'qrcode': { + const url = new URL('https://api.qrserver.com/v1/create-qr-code/'); + url.searchParams.set('data', path); + url.searchParams.set('size', '200x200'); + return url.toString(); + } } }; @@ -61,5 +68,6 @@ export const tooltipForSocial = ( case 'threads': return tooltip.shareThreads; case 'facebook': return tooltip.shareFacebook; case 'linkedin': return tooltip.shareLinkedIn; + case 'qrcode': return tooltip.shareQRCode; } };