Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

sj/export urls from extension #165

Merged
merged 3 commits into from
Jan 24, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions savvy-extension/pages/side-panel/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,13 @@
"@extension/shared": "workspace:*",
"@extension/storage": "workspace:*",
"@radix-ui/react-checkbox": "^1.1.3",
"@radix-ui/react-dialog": "^1.1.5",
"@radix-ui/react-label": "^2.1.1",
"@radix-ui/react-scroll-area": "^1.2.2",
"@radix-ui/react-select": "^2.1.4",
"@radix-ui/react-slot": "^1.1.1",
"@radix-ui/react-switch": "^1.1.2",
"@radix-ui/react-tabs": "^1.1.2",
"@radix-ui/react-toast": "^1.2.4",
"@radix-ui/react-tooltip": "^1.1.6",
"axios": "^1.7.9",
Expand Down
156 changes: 156 additions & 0 deletions savvy-extension/pages/side-panel/src/components/Copy.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import type React from "react"
import { useState, useEffect } from "react"
import { Button } from "@src/components/ui/button"
import { Dialog, DialogFooter, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from "@src/components/ui/dialog"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@src/components/ui/tabs"
import { Switch } from "@src/components/ui/switch"
import { Label } from "@src/components/ui/label"
import { Copy } from "lucide-react"
import { toast } from 'sonner';

interface HistoryItem extends chrome.history.HistoryItem {
isSelected?: boolean
}

interface CopyURLsProps {
selectedItems: HistoryItem[]
}

const extensionURL = 'https://chromewebstore.google.com/detail/savvy/jocphfjphhfbdccjfjjnbcnejmbojjlh'

export const CopyURLs: React.FC<CopyURLsProps> = ({ selectedItems }) => {
const [includeTimestamp, setIncludeTimestamp] = useState(false)
const [isOpen, setIsOpen] = useState(false)
const [copiedText, setCopiedText] = useState<boolean>(false);
const [copiedMd, setCopiedMd] = useState<boolean>(false);


const getRawText = () => {
const prependSavvy = "Here's the list of URLs (generated using Savvy):\n\n"
const prependSavvySingular = "Here's the URL (generated using Savvy):\n\n"
const text = selectedItems.map((item) => "- " + item.url).join("\n")
if (selectedItems.length === 1) {
return prependSavvySingular + text
}
return prependSavvy + text
}

const getMarkdownText = () => {
const prependSavvy = `Here's the list of URLs (generated using [Savvy](${extensionURL}))`
const prependSavvySingular = `Here's the URL (generated using [Savvy](${extensionURL}))`
const mdText = selectedItems
.map((item) => {
const title = item.title || new URL(item.url || "").hostname
const timestamp = includeTimestamp ? `${new Date(item.lastVisitTime!).toLocaleString()} ` : ``
return `- ${timestamp}[${title}](${item.url})`
})
.join("\n")

if (selectedItems.length === 1) {
return `${prependSavvySingular}\n\n${mdText}`
}
return `${prependSavvy}\n\n${mdText}`
}

const copyMdToClipboard = (text: string) => {
navigator.clipboard
.writeText(text)
.then(() => {
setCopiedMd(true)
})
.catch((err) => {
toast.error("Failed to copy text to clipboard", {
duration: 3000,
position: 'top-right',
closeButton: true,
})
console.error(err)
})
}

const copyTextToClipboard = (text: string) => {
navigator.clipboard.writeText(text).then(() => {
setCopiedText(true)
}).catch((err) => {
toast.error("Failed to copy text to clipboard", {
duration: 3000,
position: 'top-right',
closeButton: true,
})
console.error(err)
})
}


useEffect(() => {
if (copiedText) {
const timer = window.setTimeout(() => {
setCopiedText(false);
}, 3000);

// Clear the timeout if the component is unmounted before the timer is up
return () => window.clearTimeout(timer);
}
}, [copiedText]);

useEffect(() => {
if (copiedMd) {
const timer = window.setTimeout(() => {
setCopiedMd(false);
}, 3000);

// Clear the timeout if the component is unmounted before the timer is up
return () => window.clearTimeout(timer);
}
}, [copiedMd]);

return (
<>
<Dialog open={isOpen} onOpenChange={setIsOpen} modal={false}>
<DialogTrigger asChild className="w-full">
<Button variant="outline" className="mb-2 w-full" disabled={selectedItems.length === 0}>
<Copy className="mr-2 size-4" />
Copy
</Button>
</DialogTrigger>
<DialogContent className="flex w-full flex-col gap-4 overflow-scroll">
<DialogHeader>
<DialogTitle>Export History</DialogTitle>
</DialogHeader>
<Tabs defaultValue="rawtext" className="w-full">
<TabsList className="grid w-full grid-cols-2">
<TabsTrigger value="rawtext">Text</TabsTrigger>
<TabsTrigger value="markdown">Markdown</TabsTrigger>
</TabsList>
<TabsContent value="rawtext" className="flex w-full flex-col gap-8">
<div className="w-full overflow-scroll rounded-md border p-4">
<pre className="font-monospace max-h-[66vh] min-h-[33vh] font-thin">{getRawText()}</pre>

Check warning on line 127 in savvy-extension/pages/side-panel/src/components/Copy.tsx

View workflow job for this annotation

GitHub Actions / eslint

Classname 'font-monospace' is not a Tailwind CSS class!
</div>
<DialogFooter>
<Button onClick={() => copyTextToClipboard(getRawText())} className="mt-4 w-full text-white">
{ copiedText ? "Copied!" : "Copy History" }
</Button>
</DialogFooter>
</TabsContent>
<TabsContent value="markdown">
<div className="mb-4 flex items-center space-x-2">
<Switch id="timestamp-mode" checked={includeTimestamp} onCheckedChange={setIncludeTimestamp} />
<Label htmlFor="timestamp-mode">
{selectedItems.length > 1 ? "Include Timestamps" : "Include Timestamp"}
</Label>
</div>
<div className="w-full overflow-scroll rounded-md border p-4">
<pre className="font-monospace max-h-[66vh] min-h-[33vh] font-thin">{getMarkdownText()}</pre>

Check warning on line 143 in savvy-extension/pages/side-panel/src/components/Copy.tsx

View workflow job for this annotation

GitHub Actions / eslint

Classname 'font-monospace' is not a Tailwind CSS class!
</div>
<DialogFooter>
<Button onClick={() => copyMdToClipboard(getMarkdownText())} className="mt-4 w-full text-white">
{ copiedMd ? "Copied!" : "Copy Markdown" }
</Button>
</DialogFooter>
</TabsContent>
</Tabs>
</DialogContent>
</Dialog>
</>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { useLocalClient } from '@extension/shared/lib/hooks/useAPI';
import { isAxiosError } from 'axios';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@src/components/ui/tooltip';
import { ScrollArea } from '@src/components/ui/scroll-area';
import {CopyURLs} from '@src/components/Copy';

interface HistoryItem extends chrome.history.HistoryItem {
isSelected?: boolean;
Expand Down Expand Up @@ -240,7 +241,7 @@ export const HistoryViewer: React.FC<HistoryViewerProps> = () => {
};

return (
<div className="max-h-scren flex flex-col">
<div className="flex h-screen flex-col">
<div className="bg-white p-4">
<div className="flex items-center font-light">
<label htmlFor="time-range" className="mr-2 text-sm font-normal text-gray-700">
Expand Down Expand Up @@ -345,7 +346,9 @@ export const HistoryViewer: React.FC<HistoryViewerProps> = () => {
</div>

{history.length > 0 && (
<div className="sticky bottom-0 bg-white p-4">
<div className="sticky bottom-0 bg-white" >
<div className='flex gap-4 p-4'>
<CopyURLs selectedItems={history.filter(item => item.isSelected)} />
<Button
onClick={handleSave}
className="bg-primary w-full text-white"
Expand All @@ -354,6 +357,7 @@ export const HistoryViewer: React.FC<HistoryViewerProps> = () => {
Save History
</Button>
</div>
</div>
)}
<Toaster richColors position="bottom-right" expand={true} visibleToasts={2} />
</div>
Expand Down
120 changes: 120 additions & 0 deletions savvy-extension/pages/side-panel/src/components/ui/dialog.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import * as React from "react"
import * as DialogPrimitive from "@radix-ui/react-dialog"
import { X } from "lucide-react"

import { cn } from "@src/lib/utils"

const Dialog = DialogPrimitive.Root

const DialogTrigger = DialogPrimitive.Trigger

const DialogPortal = DialogPrimitive.Portal

const DialogClose = DialogPrimitive.Close

const DialogOverlay = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Overlay>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Overlay>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Overlay
ref={ref}
className={cn(
"fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0",
className
)}
{...props}
/>
))
DialogOverlay.displayName = DialogPrimitive.Overlay.displayName

const DialogContent = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<DialogPortal>
<DialogOverlay />
<DialogPrimitive.Content
ref={ref}
className={cn(
"fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg",
className
)}
{...props}
>
{children}
<DialogPrimitive.Close className="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground">
<X className="h-4 w-4" />
<span className="sr-only">Close</span>
</DialogPrimitive.Close>
</DialogPrimitive.Content>
</DialogPortal>
))
DialogContent.displayName = DialogPrimitive.Content.displayName

const DialogHeader = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col space-y-1.5 text-center sm:text-left",
className
)}
{...props}
/>
)
DialogHeader.displayName = "DialogHeader"

const DialogFooter = ({
className,
...props
}: React.HTMLAttributes<HTMLDivElement>) => (
<div
className={cn(
"flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2",
className
)}
{...props}
/>
)
DialogFooter.displayName = "DialogFooter"

const DialogTitle = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Title>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Title>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Title
ref={ref}
className={cn(
"text-lg font-semibold leading-none tracking-tight",
className
)}
{...props}
/>
))
DialogTitle.displayName = DialogPrimitive.Title.displayName

const DialogDescription = React.forwardRef<
React.ElementRef<typeof DialogPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof DialogPrimitive.Description>
>(({ className, ...props }, ref) => (
<DialogPrimitive.Description
ref={ref}
className={cn("text-sm text-muted-foreground", className)}
{...props}
/>
))
DialogDescription.displayName = DialogPrimitive.Description.displayName

export {
Dialog,
DialogPortal,
DialogOverlay,
DialogTrigger,
DialogClose,
DialogContent,
DialogHeader,
DialogFooter,
DialogTitle,
DialogDescription,
}
24 changes: 24 additions & 0 deletions savvy-extension/pages/side-panel/src/components/ui/label.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import * as React from "react"
import * as LabelPrimitive from "@radix-ui/react-label"
import { cva, type VariantProps } from "class-variance-authority"

import { cn } from "@src/lib/utils"

const labelVariants = cva(
"text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
)

const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
VariantProps<typeof labelVariants>
>(({ className, ...props }, ref) => (
<LabelPrimitive.Root
ref={ref}
className={cn(labelVariants(), className)}
{...props}
/>
))
Label.displayName = LabelPrimitive.Root.displayName

export { Label }
27 changes: 27 additions & 0 deletions savvy-extension/pages/side-panel/src/components/ui/switch.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import * as React from "react"
import * as SwitchPrimitives from "@radix-ui/react-switch"

import { cn } from "@src/lib/utils"

const Switch = React.forwardRef<
React.ElementRef<typeof SwitchPrimitives.Root>,
React.ComponentPropsWithoutRef<typeof SwitchPrimitives.Root>
>(({ className, ...props }, ref) => (
<SwitchPrimitives.Root
className={cn(
"peer inline-flex h-5 w-9 shrink-0 cursor-pointer items-center rounded-full border-2 border-transparent shadow-sm transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 focus-visible:ring-offset-background disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:bg-primary data-[state=unchecked]:bg-input",
className
)}
{...props}
ref={ref}
>
<SwitchPrimitives.Thumb
className={cn(
"pointer-events-none block h-4 w-4 rounded-full bg-background shadow-lg ring-0 transition-transform data-[state=checked]:translate-x-4 data-[state=unchecked]:translate-x-0"
)}
/>
</SwitchPrimitives.Root>
))
Switch.displayName = SwitchPrimitives.Root.displayName

export { Switch }
Loading
Loading