add dialog for copy to clipboard fallback (fixes #152)

This commit is contained in:
Henry Dollman
2024-09-02 19:37:44 -04:00
parent aa3866c8ed
commit 202a506485
6 changed files with 105 additions and 17 deletions

View File

@@ -0,0 +1,49 @@
import { useEffect, useMemo, useRef } from 'react'
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from './ui/dialog'
import { Textarea } from './ui/textarea'
import { $copyContent } from '@/lib/stores'
export default function CopyToClipboard({ content }: { content: string }) {
return (
<Dialog defaultOpen={true}>
<DialogContent className="w-[90%] rounded-lg" style={{ maxWidth: 530 }}>
<DialogHeader>
<DialogTitle>Could not copy to clipboard</DialogTitle>
<DialogDescription>Please copy the text manually.</DialogDescription>
</DialogHeader>
<CopyTextarea content={content} />
<p className="text-sm text-muted-foreground">
Clipboard API requires a secure context (https, localhost, or *.localhost)
</p>
</DialogContent>
</Dialog>
)
}
function CopyTextarea({ content }: { content: string }) {
const textareaRef = useRef<HTMLTextAreaElement>(null)
const rows = useMemo(() => {
return content.split('\n').length
}, [content])
useEffect(() => {
if (textareaRef.current) {
textareaRef.current.select()
}
}, [textareaRef])
useEffect(() => {
return () => $copyContent.set('')
}, [])
return (
<Textarea
className="font-mono overflow-hidden whitespace-pre"
rows={rows}
value={content}
readOnly
ref={textareaRef}
/>
)
}

View File

@@ -0,0 +1,23 @@
import * as React from 'react'
import { cn } from '@/lib/utils'
export interface TextareaProps extends React.TextareaHTMLAttributes<HTMLTextAreaElement> {}
const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
({ className, ...props }, ref) => {
return (
<textarea
className={cn(
'flex min-h-14 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50',
className
)}
ref={ref}
{...props}
/>
)
}
)
Textarea.displayName = 'Textarea'
export { Textarea }

View File

@@ -25,3 +25,6 @@ export const $chartTime = atom('1h') as WritableAtom<ChartTimes>
/** Container chart filter */ /** Container chart filter */
export const $containerFilter = atom('') export const $containerFilter = atom('')
/** Fallback copy to clipboard dialog content */
export const $copyContent = atom('')

View File

@@ -1,7 +1,7 @@
import { toast } from '@/components/ui/use-toast' import { toast } from '@/components/ui/use-toast'
import { type ClassValue, clsx } from 'clsx' import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import { $alerts, $systems, pb } from './stores' import { $alerts, $copyContent, $systems, pb } from './stores'
import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types' import { AlertRecord, ChartTimeData, ChartTimes, SystemRecord } from '@/types'
import { RecordModel, RecordSubscription } from 'pocketbase' import { RecordModel, RecordSubscription } from 'pocketbase'
import { WritableAtom } from 'nanostores' import { WritableAtom } from 'nanostores'
@@ -22,10 +22,7 @@ export async function copyToClipboard(content: string) {
description: 'Copied to clipboard', description: 'Copied to clipboard',
}) })
} catch (e: any) { } catch (e: any) {
prompt( $copyContent.set(content)
'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:',
content
)
} }
} }

View File

@@ -3,7 +3,14 @@ import React, { Suspense, lazy, useEffect } from 'react'
import ReactDOM from 'react-dom/client' import ReactDOM from 'react-dom/client'
import Home from './components/routes/home.tsx' import Home from './components/routes/home.tsx'
import { ThemeProvider } from './components/theme-provider.tsx' import { ThemeProvider } from './components/theme-provider.tsx'
import { $authenticated, $systems, pb, $publicKey, $hubVersion } from './lib/stores.ts' import {
$authenticated,
$systems,
pb,
$publicKey,
$hubVersion,
$copyContent,
} from './lib/stores.ts'
import { ModeToggle } from './components/mode-toggle.tsx' import { ModeToggle } from './components/mode-toggle.tsx'
import { import {
cn, cn,
@@ -42,6 +49,7 @@ import { AddSystemButton } from './components/add-system.tsx'
// const ServerDetail = lazy(() => import('./components/routes/system.tsx')) // const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
const CommandPalette = lazy(() => import('./components/command-palette.tsx')) const CommandPalette = lazy(() => import('./components/command-palette.tsx'))
const LoginPage = lazy(() => import('./components/login/login.tsx')) const LoginPage = lazy(() => import('./components/login/login.tsx'))
const CopyToClipboardDialog = lazy(() => import('./components/copy-to-clipboard.tsx'))
const App = () => { const App = () => {
const page = useStore($router) const page = useStore($router)
@@ -96,6 +104,7 @@ const App = () => {
const Layout = () => { const Layout = () => {
const authenticated = useStore($authenticated) const authenticated = useStore($authenticated)
const copyContent = useStore($copyContent)
if (!authenticated) { if (!authenticated) {
return ( return (
@@ -187,16 +196,23 @@ const Layout = () => {
<Suspense> <Suspense>
<CommandPalette /> <CommandPalette />
</Suspense> </Suspense>
{copyContent && (
<Suspense>
<CopyToClipboardDialog content={copyContent} />
</Suspense>
)}
</div> </div>
</> </>
) )
} }
ReactDOM.createRoot(document.getElementById('app')!).render( ReactDOM.createRoot(document.getElementById('app')!).render(
<React.StrictMode> // strict mode in dev mounts / unmounts components twice
// and breaks the clipboard dialog
//<React.StrictMode>
<ThemeProvider> <ThemeProvider>
<Layout /> <Layout />
<Toaster /> <Toaster />
</ThemeProvider> </ThemeProvider>
</React.StrictMode> //</React.StrictMode>
) )

View File

@@ -16,12 +16,12 @@ module.exports = {
'2xl': '1400px', '2xl': '1400px',
}, },
}, },
extend: {
fontFamily: { fontFamily: {
sans: 'Inter, sans-serif', sans: 'Inter, sans-serif',
// body: ['Inter', 'sans-serif'], // body: ['Inter', 'sans-serif'],
// display: ['Inter', 'sans-serif'], // display: ['Inter', 'sans-serif'],
}, },
extend: {
screens: { screens: {
xs: '425px', xs: '425px',
}, },