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
This commit is contained in:
Carlo Bortolan 2026-01-12 02:11:27 +09:00 committed by GitHub
parent 822a1b86e5
commit 43c6bceb94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 75 additions and 18 deletions

View File

@ -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`

View File

@ -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));

View File

@ -912,14 +912,14 @@ export default function AdminAppConfigurationClient({
{renderEnvVars(['NEXT_PUBLIC_ALLOW_PUBLIC_DOWNLOADS'])}
</ChecklistRow>
<ChecklistRow
title="Social networks"
title="Social networks and sharing"
status={hasSocialKeys}
optional
>
{renderOrderedKeyList(socialKeys, SOCIAL_KEYS)}
<div>
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)})
</div>

View File

@ -96,6 +96,7 @@ export const TEXT: I18N = {
shareThreads: 'Threads এ শেয়ার করুন',
shareFacebook: 'Facebook এ শেয়ার করুন',
shareLinkedIn: 'LinkedIn এ শেয়ার করুন',
shareQRCode: 'QR কোড টগল করুন',
},
theme: {
theme: 'থিম',

View File

@ -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',

View File

@ -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',

View File

@ -96,6 +96,7 @@ export const TEXT: I18N = {
shareThreads: 'Threads पर साझा करें',
shareFacebook: 'Facebook पर साझा करें',
shareLinkedIn: 'LinkedIn पर साझा करें',
shareQRCode: 'QR कोड टॉगल करें',
},
theme: {
theme: 'थीम',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -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',

View File

@ -96,6 +96,7 @@ export const TEXT: I18N = {
shareThreads: '在 Threads 上分享',
shareFacebook: '在 Facebook 上分享',
shareLinkedIn: '在 LinkedIn 上分享',
shareQRCode: '切换二维码',
},
theme: {
theme: '主题',

View File

@ -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}
</div>
</div>}
{children}
{!showQR ? (
<>{children}</>
) : (
<div className="flex flex-col items-center gap-4 p-4">
<div className={clsx(
'p-3 bg-white rounded-2xl shadow-lg border',
'flex items-center justify-center',
)}>
<Image
/* eslint-disable-next-line max-len */
src={`https://api.qrserver.com/v1/create-qr-code/?size=300x300&data=${encodeURIComponent(pathShare)}`}
alt="QR Code"
className="rounded-xl bg-white"
width={300}
height={300}
/>
</div>
</div>
)}
<div className="flex items-stretch h-10 gap-2">
<div className={clsx(
'rounded-md',
@ -119,16 +139,26 @@ export default function ShareModal({
)}
</div>
{SOCIAL_NETWORKS.map(key =>
<SocialButton
key={key}
socialKey={key}
path={pathShare}
text={socialText}
className={clsx(
'h-full',
BUTTON_COLOR_CLASSNAMES,
)}
/>)}
key === 'qrcode' ? (
renderButton(
<TbQrcode size={18} />,
() => setShowQR(q => !q),
false,
appText.tooltip.shareQRCode,
)
) : (
<SocialButton
key={key}
socialKey={key}
path={pathShare}
text={socialText}
className={clsx(
'h-full',
BUTTON_COLOR_CLASSNAMES,
)}
/>
),
)}
{typeof navigator !== 'undefined' && navigator.share &&
renderButton(
<IoArrowUp size={18} />,

View File

@ -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 <FaThreads size={18} />;
case 'facebook': return <FaFacebookF size={14} />;
case 'linkedin': return <FaLinkedin size={16} />;
case 'qrcode': return <TbQrcode size={16} />;
}
};

View File

@ -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;
}
};