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 */
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 { type ClassValue, clsx } from 'clsx'
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 { RecordModel, RecordSubscription } from 'pocketbase'
import { WritableAtom } from 'nanostores'
@@ -22,10 +22,7 @@ export async function copyToClipboard(content: string) {
description: 'Copied to clipboard',
})
} catch (e: any) {
prompt(
'Automatic copy requires a secure context (https, localhost, or *.localhost). Please copy manually:',
content
)
$copyContent.set(content)
}
}

View File

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

View File

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