diff --git a/app/components/building-blocks/copy-to-clipboard-button.tsx b/app/components/building-blocks/copy-to-clipboard-button.tsx new file mode 100644 index 00000000..552e0abd --- /dev/null +++ b/app/components/building-blocks/copy-to-clipboard-button.tsx @@ -0,0 +1,19 @@ +import { toast } from 'sonner' +import { Icon } from '../ui/icon.tsx' + +export function CopyToClipboardButton({ url }: { url: string }) { + const copyToClipboard = async () => { + try { + await navigator.clipboard.writeText(url) + toast('Copied URL to clipboard') + } catch (err) { + console.error('Failed to copy:', err) + } + } + + return ( + + ) +} diff --git a/app/components/building-blocks/post-action-bar.tsx b/app/components/building-blocks/post-action-bar.tsx index 78a9e29f..cc9ad2ca 100644 --- a/app/components/building-blocks/post-action-bar.tsx +++ b/app/components/building-blocks/post-action-bar.tsx @@ -4,6 +4,7 @@ import { useState, type ChangeEvent, useRef, + useEffect, } from 'react' import TextareaAutosize from 'react-textarea-autosize' import { Icon } from '#app/components/ui/icon.tsx' @@ -25,6 +26,13 @@ import { toImmutableReplyTree } from '#app/modules/posts/ranking/ranking-utils.t import { type TreeContext } from '#app/routes/post.$postId.tsx' import { invariant } from '#app/utils/misc.tsx' import { useOptionalUser } from '#app/utils/user.ts' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from '../ui/dropdown-menu.tsx' +import { CopyToClipboardButton } from './copy-to-clipboard-button.tsx' export function PostActionBar({ replyTree, @@ -45,6 +53,7 @@ export function PostActionBar({ const loggedIn: boolean = user !== null const isAdminUser: boolean = user ? Boolean(user.isAdmin) : false const [showAdminUI, setShowAdminUI] = useState(false) + const isPostInView = treeContext.targetPostId === post.id const targetPostState = treeContext.commentTreeState.posts[treeContext.targetPostId] @@ -150,6 +159,11 @@ export function PostActionBar({ const focusButtonColor = isFocused ? 'bg-rose-800/100 text-white' : '' + const [currentUrl, setCurrentUrl] = useState('') + useEffect(() => { + setCurrentUrl(`${location.origin}${location.pathname}`) + }, []) + return ( <>
@@ -203,6 +217,18 @@ export function PostActionBar({ Reply )} + {isPostInView && ( + + + Share + + + + + + + + )} {isAdminUser && (showAdminUI ? ( + +const Toaster = ({ ...props }: ToasterProps) => { + const { theme = 'system' } = useTheme() + + return ( + + ) +} + +export { Toaster } diff --git a/app/components/ui/icons/name.d.ts b/app/components/ui/icons/name.d.ts index 8b0b11a0..1dca0edc 100644 --- a/app/components/ui/icons/name.d.ts +++ b/app/components/ui/icons/name.d.ts @@ -11,6 +11,7 @@ export type IconName = | 'chevron-right' | 'chevron-up' | 'clock' + | 'copy' | 'counter-clockwise-clock' | 'cross-1' | 'dots-horizontal' @@ -42,6 +43,7 @@ export type IconName = | 'quote' | 'reload' | 'reset' + | 'share' | 'sun' | 'target' | 'thick-arrow-down-solid' diff --git a/app/components/ui/icons/sprite.svg b/app/components/ui/icons/sprite.svg index 85a7b52d..eeab39e5 100644 --- a/app/components/ui/icons/sprite.svg +++ b/app/components/ui/icons/sprite.svg @@ -48,6 +48,7 @@ fill="currentColor" > + + diff --git a/other/svg-icons/share.svg b/other/svg-icons/share.svg new file mode 100644 index 00000000..f66118a5 --- /dev/null +++ b/other/svg-icons/share.svg @@ -0,0 +1 @@ + diff --git a/package-lock.json b/package-lock.json index 74110c1d..0c213fc3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -65,6 +65,7 @@ "mock-aws-s3": "^4.0.2", "moment": "^2.29.4", "morgan": "^1.10.0", + "next-themes": "^0.4.3", "nock": "^13.5.4", "openai": "^4.56.1", "pino": "^9.2.0", @@ -79,7 +80,7 @@ "remix-utils": "^7.1.0", "sanitize-html": "^2.13.1", "set-cookie-parser": "^2.6.0", - "sonner": "^1.0.3", + "sonner": "^1.7.0", "spin-delay": "^1.2.0", "tailwind-merge": "^1.14.0", "tailwindcss": "^3.3.3", @@ -14977,6 +14978,15 @@ "node": ">= 0.6" } }, + "node_modules/next-themes": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/next-themes/-/next-themes-0.4.3.tgz", + "integrity": "sha512-nG84VPkTdUHR2YeD89YchvV4I9RbiMAql3GiLEQlPvq1ioaqPaIReK+yMRdg/zgiXws620qS1rU30TiWmmG9lA==", + "peerDependencies": { + "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" + } + }, "node_modules/nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -18399,12 +18409,12 @@ } }, "node_modules/sonner": { - "version": "1.4.41", - "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.4.41.tgz", - "integrity": "sha512-uG511ggnnsw6gcn/X+YKkWPo5ep9il9wYi3QJxHsYe7yTZ4+cOd1wuodOUmOpFuXL+/RE3R04LczdNCDygTDgQ==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/sonner/-/sonner-1.7.0.tgz", + "integrity": "sha512-W6dH7m5MujEPyug3lpI2l3TC3Pp1+LTgK0Efg+IHDrBbtEjyCmCHHo6yfNBOsf1tFZ6zf+jceWwB38baC8yO9g==", "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" + "react": "^18.0.0 || ^19.0.0 || ^19.0.0-rc", + "react-dom": "^18.0.0 || ^19.0.0 || ^19.0.0-rc" } }, "node_modules/source-map": { diff --git a/package.json b/package.json index bd7e5bcf..5cc01ca9 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "mock-aws-s3": "^4.0.2", "moment": "^2.29.4", "morgan": "^1.10.0", + "next-themes": "^0.4.3", "nock": "^13.5.4", "openai": "^4.56.1", "pino": "^9.2.0", @@ -106,7 +107,7 @@ "remix-utils": "^7.1.0", "sanitize-html": "^2.13.1", "set-cookie-parser": "^2.6.0", - "sonner": "^1.0.3", + "sonner": "^1.7.0", "spin-delay": "^1.2.0", "tailwind-merge": "^1.14.0", "tailwindcss": "^3.3.3",