From 07251455621e6b85f936a733b278e4825b7a204d Mon Sep 17 00:00:00 2001 From: TZGyn Date: Sun, 17 Sep 2023 00:29:06 +0800 Subject: [PATCH] Update frontend shorteners table to click to copy --- react-frontend/bun.lockb | Bin 131432 -> 132880 bytes react-frontend/package.json | 1 + react-frontend/src/App.tsx | 66 ++++-- react-frontend/src/components/ui/card.tsx | 79 +++++++ react-frontend/src/components/ui/toast.tsx | 127 ++++++++++++ react-frontend/src/components/ui/toaster.tsx | 33 +++ react-frontend/src/components/ui/use-toast.ts | 192 ++++++++++++++++++ 7 files changed, 480 insertions(+), 18 deletions(-) create mode 100644 react-frontend/src/components/ui/card.tsx create mode 100644 react-frontend/src/components/ui/toast.tsx create mode 100644 react-frontend/src/components/ui/toaster.tsx create mode 100644 react-frontend/src/components/ui/use-toast.ts diff --git a/react-frontend/bun.lockb b/react-frontend/bun.lockb index 4dcb424478e7bf4f0f7437e433ed910dace18162..e791da64eb7584db0c1174228e2495447c2cccc8 100755 GIT binary patch delta 30105 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 delta 29272 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 K4%p6i#Qq;5FsX(B diff --git a/react-frontend/package.json b/react-frontend/package.json index ff97f0c..46b0575 100644 --- a/react-frontend/package.json +++ b/react-frontend/package.json @@ -15,6 +15,7 @@ "@radix-ui/react-dropdown-menu": "^2.0.5", "@radix-ui/react-label": "^2.0.2", "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-toast": "^1.1.4", "class-variance-authority": "^0.7.0", "clsx": "^2.0.0", "lucide-react": "^0.274.0", diff --git a/react-frontend/src/App.tsx b/react-frontend/src/App.tsx index 0b862c1..3fc580d 100644 --- a/react-frontend/src/App.tsx +++ b/react-frontend/src/App.tsx @@ -20,9 +20,13 @@ import { TableHeader, TableRow, } from '@/components/ui/table' +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' +import { Toaster } from '@/components/ui/toaster' +import { useToast } from '@/components/ui/use-toast' + import { useEffect, useState } from 'react' -const backend_url = 'http://192.168.100.40:1234' +const backend_url = 'http://192.168.100.40:3000' type Shortener = { id: number @@ -57,6 +61,7 @@ export default function App() {
+ ) } @@ -79,6 +84,7 @@ const CreateShortener = ({ }: { getShorteners: () => Promise }) => { + const [isOpen, setIsOpen] = useState(false) const [link, setLink] = useState('') const addShortener = async () => { await fetch(backend_url + '/link', { @@ -92,10 +98,13 @@ const CreateShortener = ({ }).then(() => { getShorteners() setLink('') + setIsOpen(false) }) } return ( - + @@ -132,22 +141,43 @@ const CreateShortener = ({ } const ShortenerTable = ({ shorteners }: { shorteners: Shortener[] }) => { + const { toast } = useToast() + const copyLinkToClipboard = async (code: string) => { + await navigator.clipboard.writeText(backend_url + '/' + code) + console.log(code) + toast({ + title: 'Link Copied', + description: `Copied ${backend_url + '/' + code} To Clipboard`, + }) + } return ( - - - - Link - Shortener - - - - {shorteners.map((shortener) => ( - - {shortener.link} - {shortener.code} - - ))} - -
+ + + Shorteners + + + + + + Link + Shortener + + + + {shorteners.map((shortener) => ( + + {shortener.link} + + copyLinkToClipboard(shortener.code) + }> + {shortener.code} + + + ))} + +
+
+
) } diff --git a/react-frontend/src/components/ui/card.tsx b/react-frontend/src/components/ui/card.tsx new file mode 100644 index 0000000..afa13ec --- /dev/null +++ b/react-frontend/src/components/ui/card.tsx @@ -0,0 +1,79 @@ +import * as React from "react" + +import { cn } from "@/lib/utils" + +const Card = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +Card.displayName = "Card" + +const CardHeader = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardHeader.displayName = "CardHeader" + +const CardTitle = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardTitle.displayName = "CardTitle" + +const CardDescription = React.forwardRef< + HTMLParagraphElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardDescription.displayName = "CardDescription" + +const CardContent = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +

+)) +CardContent.displayName = "CardContent" + +const CardFooter = React.forwardRef< + HTMLDivElement, + React.HTMLAttributes +>(({ className, ...props }, ref) => ( +
+)) +CardFooter.displayName = "CardFooter" + +export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } diff --git a/react-frontend/src/components/ui/toast.tsx b/react-frontend/src/components/ui/toast.tsx new file mode 100644 index 0000000..a822477 --- /dev/null +++ b/react-frontend/src/components/ui/toast.tsx @@ -0,0 +1,127 @@ +import * as React from "react" +import * as ToastPrimitives from "@radix-ui/react-toast" +import { cva, type VariantProps } from "class-variance-authority" +import { X } from "lucide-react" + +import { cn } from "@/lib/utils" + +const ToastProvider = ToastPrimitives.Provider + +const ToastViewport = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastViewport.displayName = ToastPrimitives.Viewport.displayName + +const toastVariants = cva( + "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-6 pr-8 shadow-lg transition-all data-[swipe=cancel]:translate-x-0 data-[swipe=end]:translate-x-[var(--radix-toast-swipe-end-x)] data-[swipe=move]:translate-x-[var(--radix-toast-swipe-move-x)] data-[swipe=move]:transition-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[swipe=end]:animate-out data-[state=closed]:fade-out-80 data-[state=closed]:slide-out-to-right-full data-[state=open]:slide-in-from-top-full data-[state=open]:sm:slide-in-from-bottom-full", + { + variants: { + variant: { + default: "border bg-background text-foreground", + destructive: + "destructive group border-destructive bg-destructive text-destructive-foreground", + }, + }, + defaultVariants: { + variant: "default", + }, + } +) + +const Toast = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef & + VariantProps +>(({ className, variant, ...props }, ref) => { + return ( + + ) +}) +Toast.displayName = ToastPrimitives.Root.displayName + +const ToastAction = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastAction.displayName = ToastPrimitives.Action.displayName + +const ToastClose = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + + + +)) +ToastClose.displayName = ToastPrimitives.Close.displayName + +const ToastTitle = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastTitle.displayName = ToastPrimitives.Title.displayName + +const ToastDescription = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +ToastDescription.displayName = ToastPrimitives.Description.displayName + +type ToastProps = React.ComponentPropsWithoutRef + +type ToastActionElement = React.ReactElement + +export { + type ToastProps, + type ToastActionElement, + ToastProvider, + ToastViewport, + Toast, + ToastTitle, + ToastDescription, + ToastClose, + ToastAction, +} diff --git a/react-frontend/src/components/ui/toaster.tsx b/react-frontend/src/components/ui/toaster.tsx new file mode 100644 index 0000000..a2209ba --- /dev/null +++ b/react-frontend/src/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/toast" +import { useToast } from "@/components/ui/use-toast" + +export function Toaster() { + const { toasts } = useToast() + + return ( + + {toasts.map(function ({ id, title, description, action, ...props }) { + return ( + +
+ {title && {title}} + {description && ( + {description} + )} +
+ {action} + +
+ ) + })} + +
+ ) +} diff --git a/react-frontend/src/components/ui/use-toast.ts b/react-frontend/src/components/ui/use-toast.ts new file mode 100644 index 0000000..90d8959 --- /dev/null +++ b/react-frontend/src/components/ui/use-toast.ts @@ -0,0 +1,192 @@ +// Inspired by react-hot-toast library +import * as React from "react" + +import type { + ToastActionElement, + ToastProps, +} from "@/components/ui/toast" + +const TOAST_LIMIT = 1 +const TOAST_REMOVE_DELAY = 1000000 + +type ToasterToast = ToastProps & { + id: string + title?: React.ReactNode + description?: React.ReactNode + action?: ToastActionElement +} + +const actionTypes = { + ADD_TOAST: "ADD_TOAST", + UPDATE_TOAST: "UPDATE_TOAST", + DISMISS_TOAST: "DISMISS_TOAST", + REMOVE_TOAST: "REMOVE_TOAST", +} as const + +let count = 0 + +function genId() { + count = (count + 1) % Number.MAX_VALUE + return count.toString() +} + +type ActionType = typeof actionTypes + +type Action = + | { + type: ActionType["ADD_TOAST"] + toast: ToasterToast + } + | { + type: ActionType["UPDATE_TOAST"] + toast: Partial + } + | { + type: ActionType["DISMISS_TOAST"] + toastId?: ToasterToast["id"] + } + | { + type: ActionType["REMOVE_TOAST"] + toastId?: ToasterToast["id"] + } + +interface State { + toasts: ToasterToast[] +} + +const toastTimeouts = new Map>() + +const addToRemoveQueue = (toastId: string) => { + if (toastTimeouts.has(toastId)) { + return + } + + const timeout = setTimeout(() => { + toastTimeouts.delete(toastId) + dispatch({ + type: "REMOVE_TOAST", + toastId: toastId, + }) + }, TOAST_REMOVE_DELAY) + + toastTimeouts.set(toastId, timeout) +} + +export const reducer = (state: State, action: Action): State => { + switch (action.type) { + case "ADD_TOAST": + return { + ...state, + toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT), + } + + case "UPDATE_TOAST": + return { + ...state, + toasts: state.toasts.map((t) => + t.id === action.toast.id ? { ...t, ...action.toast } : t + ), + } + + case "DISMISS_TOAST": { + const { toastId } = action + + // ! Side effects ! - This could be extracted into a dismissToast() action, + // but I'll keep it here for simplicity + if (toastId) { + addToRemoveQueue(toastId) + } else { + state.toasts.forEach((toast) => { + addToRemoveQueue(toast.id) + }) + } + + return { + ...state, + toasts: state.toasts.map((t) => + t.id === toastId || toastId === undefined + ? { + ...t, + open: false, + } + : t + ), + } + } + case "REMOVE_TOAST": + if (action.toastId === undefined) { + return { + ...state, + toasts: [], + } + } + return { + ...state, + toasts: state.toasts.filter((t) => t.id !== action.toastId), + } + } +} + +const listeners: Array<(state: State) => void> = [] + +let memoryState: State = { toasts: [] } + +function dispatch(action: Action) { + memoryState = reducer(memoryState, action) + listeners.forEach((listener) => { + listener(memoryState) + }) +} + +type Toast = Omit + +function toast({ ...props }: Toast) { + const id = genId() + + const update = (props: ToasterToast) => + dispatch({ + type: "UPDATE_TOAST", + toast: { ...props, id }, + }) + const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id }) + + dispatch({ + type: "ADD_TOAST", + toast: { + ...props, + id, + open: true, + onOpenChange: (open) => { + if (!open) dismiss() + }, + }, + }) + + return { + id: id, + dismiss, + update, + } +} + +function useToast() { + const [state, setState] = React.useState(memoryState) + + React.useEffect(() => { + listeners.push(setState) + return () => { + const index = listeners.indexOf(setState) + if (index > -1) { + listeners.splice(index, 1) + } + } + }, [state]) + + return { + ...state, + toast, + dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }), + } +} + +export { useToast, toast }