Skip to content

Commit

Permalink
[#398] Add ability to share a post by click (#449)
Browse files Browse the repository at this point in the history
* [#398] Add Popover component, change Page and ShareButton components

* [#398] fix ShareButton, add share button to Page, add Popover component, add custom styles to Tailwind, install @headlessui/react

* [#398] fix ShareButton, add share button to Page, add Popup component, use @radix-ui/react-popover

* [#398] unite ShareButton and Popup components to ShareButtonWithPopover, some fix

* [#398] rename ShareButtonWithPopover to ShareButton, add translates (but not sure to make it correct), refactor in ShareButton some fix

* [#398] change translates approach, use translate in Page and add to ShareButton as prop

* [#398] change translate locale

* [#455] add hook useTimer, use hook in ShareButton, change prop name content => tooltipContent

* [#455] add descriptions to hook useTimer, remove duplicate unmount effect from ShareButton
  • Loading branch information
Stonek79 authored Nov 6, 2023
1 parent 4e38be3 commit 66c0df2
Show file tree
Hide file tree
Showing 8 changed files with 551 additions and 23 deletions.
1 change: 1 addition & 0 deletions apps/blog/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"@amplitude/analytics-browser": "^2.1.2",
"@radix-ui/react-dropdown-menu": "^2.0.4",
"@radix-ui/react-popover": "^1.0.7",
"clsx": "^1.2.1",
"contentlayer": "^0.3.4",
"date-fns": "^2.29.3",
Expand Down
9 changes: 7 additions & 2 deletions apps/blog/src/app/[locale]/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,12 @@
@tailwind utilities;

@layer base {
h1, h2, h3, h4, h5 {
font-family: var(--font-archerusFeral), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
h1,
h2,
h3,
h4,
h5 {
font-family: var(--font-archerusFeral), ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial,
'Noto Sans', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
}
}
15 changes: 13 additions & 2 deletions apps/blog/src/app/[locale]/posts/[slug]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { generateFullUrl } from '@/utils/generateFullUrl'
import { memeberToPostAuthor } from '@/utils/memeberToPostAuthor'
import { formatDate } from '@/utils/formatDate'
import type { Language } from '@/i18n/i18n.settings'
import { ShareButton } from '@/components/ShareButton'
import { useTranslation } from '@/i18n'

interface BlogProps {
params: {
Expand Down Expand Up @@ -61,9 +63,10 @@ export const generateMetadata = ({ params }: BlogProps): Metadata => {
}
}

export default function Post({ params }: BlogProps) {
export default async function Post({ params }: BlogProps) {
const post = allBlogPostsWithTranslates.find(post => post.slug === params.slug && isPostShouldBePickedByLocale(post, params.locale))
const postAuthor = allMemebers.find(memeber => memeber.username === post?.author)
const { t } = await useTranslation(params.locale, 'post')

if (!post || !postAuthor) {
notFound()
Expand All @@ -88,7 +91,15 @@ export default function Post({ params }: BlogProps) {
<Image fill className="object-contain rounded-lg" src={post.image} alt={post.imageDescription || post.title} />
</div>
<div className="col-start-1 col-span-1 max-w-full md:max-w-5xl flex flex-col mt-6">
<p className="text-gray-600 text-sm">{formatDate(post.publishedAt, params.locale)}</p>
<div className="col-start-1 col-span-1 max-w-full md:max-w-5xl flex flex-col mt-6">
<div className="flex justify-between items-center mb-4">
<p className="text-gray-600 text-sm">{formatDate(post.publishedAt, params.locale)}</p>
<ShareButton
tooltipContent={t('copied')}
shareData={{ title: post.title, text: post.summary, url: generateFullUrl(`/${params.locale}/posts/${post.slug}`) }}
/>
</div>
</div>
<h1 className="font-bold text-3xl mt-6 lg:text-5xl lg:font-extrabold">{post.title}</h1>
{post.tags ? (
<div className="my-8">
Expand Down
90 changes: 73 additions & 17 deletions apps/blog/src/components/ShareButton/ShareButton.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,81 @@
'use client'

export function ShareButton({ shareData }: { shareData: ShareData }) {
const handleClick = async () => {
import React, { useCallback, useState } from 'react'
import * as Popover from '@radix-ui/react-popover'
import { useTimer } from '@/utils/hooks/useTimer'

const popoverTimer = 1000

export const ShareButton = ({ shareData, tooltipContent }: { shareData: ShareData; tooltipContent: string }) => {
const [open, setOpen] = useState(false)

const setPopoverClosed = useCallback(() => setOpen(false), [setOpen])
const { startTimer, stopTimer } = useTimer(setPopoverClosed, popoverTimer)

const handlePopoverOpen = useCallback(() => {
if (open) {
setOpen(false)
stopTimer()
} else {
setOpen(true)
startTimer()
}
}, [startTimer, stopTimer, open])

/**
* Handles the click event for the button.
* If the browser does not support the share API, it will copy the text (url/text/title), otherwise it will open the share dialog.
* Dialog will disappear after "popoverTimer" ms or if the user clicks outside the popover
*
* @async
* @return {void}
*/
const handleClick = useCallback(async () => {
try {
await navigator.share(shareData)
} catch (err) {
await navigator.clipboard.writeText(shareData.url || window.location.origin)
if (!navigator.share) {
await navigator.clipboard.writeText(shareData.url || shareData.text || shareData.title || '')
handlePopoverOpen()
} else {
await navigator.share(shareData)
}
} catch (error: unknown) {
stopTimer()

if (error instanceof Error && error.name === 'AbortError') {
return
}

console.error(error)
}
}
}, [handlePopoverOpen, stopTimer, shareData])

return (
<button
onClick={handleClick}
className="relative after:top-1/2 after:left-1/2 after:absolute after:-translate-x-1/2 after:-translate-y-1/2 after:w-[150%] after:h-[150%] transition-colors transition-opacity opacity-30 hover:text-memebattleYellow hover:opacity-100"
>
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" fill="none">
<path
stroke="currentColor"
d="m6.523 12.622 5.454 3.506m0-10.256L6.523 9.378M17.5 17.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM7 11a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm10.5-6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</button>
<Popover.Root open={open} onOpenChange={handleClick}>
<Popover.Trigger className="flex" asChild>
<svg
className="items-center justify-end opacity-30 hover:text-memebattleYellow hover:opacity-100"
xmlns="http://www.w3.org/2000/svg"
width="22"
height="22"
fill="none"
>
<path
stroke="currentColor"
d="m6.523 12.622 5.454 3.506m0-10.256L6.523 9.378M17.5 17.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0ZM7 11a3 3 0 1 1-6 0 3 3 0 0 1 6 0Zm10.5-6.75a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"
/>
</svg>
</Popover.Trigger>
<Popover.Portal>
<Popover.Content
className="flex relative w-fit p-3 whitespace-nowrap text-sm rounded-lg rounded-br-lg bg-gray-200 focus:outline-none"
side="top"
align="end"
sideOffset={5}
>
{tooltipContent}
<Popover.Arrow className="fill-gray-200" width={10} height={10} />
</Popover.Content>
</Popover.Portal>
</Popover.Root>
)
}
3 changes: 2 additions & 1 deletion apps/blog/src/i18n/locales/en/post.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"tocHeader": "Contents"
"tocHeader": "Contents",
"copied": "Copied to clipboard"
}
3 changes: 2 additions & 1 deletion apps/blog/src/i18n/locales/ru/post.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
{
"tocHeader": "Оглавление"
"tocHeader": "Оглавление",
"copied": "Ссылка скопирована"
}
35 changes: 35 additions & 0 deletions apps/blog/src/utils/hooks/useTimer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { useEffect, useRef, useCallback } from 'react'

interface TimerFunctions {
startTimer: () => void
stopTimer: () => void
}

/**
* Generates a timer hook that allows the user to start and stop a timer.
*
* @param {() => void} callback - The callback function to be executed when the timer completes.
* @param {number} delay - The delay in milliseconds before the timer executes the callback function. Default is 2000.
* @return {TimerFunctions} - An object containing the startTimer and stopTimer functions, which can be used to start and stop the timer respectively.
*/
export function useTimer(callback: () => void, delay = 2000): TimerFunctions {
const timerRef = useRef<NodeJS.Timeout | null>(null)

const startTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current)
}
timerRef.current = setTimeout(callback, delay)
}, [callback, delay])

const stopTimer = useCallback(() => {
if (timerRef.current) {
clearTimeout(timerRef.current)
timerRef.current = null
}
}, [])

useEffect(() => stopTimer, [stopTimer])

return { startTimer, stopTimer }
}
Loading

1 comment on commit 66c0df2

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage report for apps/ligretto-gameplay-backend

St.
Category Percentage Covered / Total
🔴 Statements 49.8% 373/749
🔴 Branches 25.24% 26/103
🔴 Functions 26.7% 55/206
🔴 Lines 47.47% 310/653

Test suite run success

12 tests passing in 1 suite.

Report generated by 🧪jest coverage report action from 66c0df2

Please sign in to comment.