From 93a8a829d1e1c1cd9c959a974d10fa61237cad03 Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 16:44:25 -0800 Subject: [PATCH 01/13] added new shadcn components and create new utils functions --- @/components/ui/tabs.tsx | 53 +++ @/components/ui/toast.tsx | 127 +++++++ @/components/ui/toaster.tsx | 33 ++ @/components/ui/tooltip.tsx | 28 ++ @/hooks/use-toast.ts | 194 ++++++++++ app/client/convertUtcDateToLocalDateString.ts | 15 + app/client/fallbackImageSource.ts | 2 + app/client/index.ts | 2 + app/components/CopyToClipboardButton.tsx | 69 ++++ app/hooks/useLoggedInUser.ts | 20 +- app/root.tsx | 42 ++- package-lock.json | 353 +++++++++++++++++- package.json | 5 +- 13 files changed, 912 insertions(+), 31 deletions(-) create mode 100644 @/components/ui/tabs.tsx create mode 100644 @/components/ui/toast.tsx create mode 100644 @/components/ui/toaster.tsx create mode 100644 @/components/ui/tooltip.tsx create mode 100644 @/hooks/use-toast.ts create mode 100644 app/client/convertUtcDateToLocalDateString.ts create mode 100644 app/client/fallbackImageSource.ts create mode 100644 app/client/index.ts create mode 100644 app/components/CopyToClipboardButton.tsx diff --git a/@/components/ui/tabs.tsx b/@/components/ui/tabs.tsx new file mode 100644 index 0000000..f57fffd --- /dev/null +++ b/@/components/ui/tabs.tsx @@ -0,0 +1,53 @@ +import * as React from "react" +import * as TabsPrimitive from "@radix-ui/react-tabs" + +import { cn } from "@/lib/utils" + +const Tabs = TabsPrimitive.Root + +const TabsList = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsList.displayName = TabsPrimitive.List.displayName + +const TabsTrigger = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsTrigger.displayName = TabsPrimitive.Trigger.displayName + +const TabsContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, ...props }, ref) => ( + +)) +TabsContent.displayName = TabsPrimitive.Content.displayName + +export { Tabs, TabsList, TabsTrigger, TabsContent } diff --git a/@/components/ui/toast.tsx b/@/components/ui/toast.tsx new file mode 100644 index 0000000..a822477 --- /dev/null +++ b/@/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/@/components/ui/toaster.tsx b/@/components/ui/toaster.tsx new file mode 100644 index 0000000..5887f08 --- /dev/null +++ b/@/components/ui/toaster.tsx @@ -0,0 +1,33 @@ +import { useToast } from "@/hooks/use-toast"; +import { + Toast, + ToastClose, + ToastDescription, + ToastProvider, + ToastTitle, + ToastViewport, +} from "@/components/ui/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/@/components/ui/tooltip.tsx b/@/components/ui/tooltip.tsx new file mode 100644 index 0000000..e121f0a --- /dev/null +++ b/@/components/ui/tooltip.tsx @@ -0,0 +1,28 @@ +import * as React from "react" +import * as TooltipPrimitive from "@radix-ui/react-tooltip" + +import { cn } from "@/lib/utils" + +const TooltipProvider = TooltipPrimitive.Provider + +const Tooltip = TooltipPrimitive.Root + +const TooltipTrigger = TooltipPrimitive.Trigger + +const TooltipContent = React.forwardRef< + React.ElementRef, + React.ComponentPropsWithoutRef +>(({ className, sideOffset = 4, ...props }, ref) => ( + +)) +TooltipContent.displayName = TooltipPrimitive.Content.displayName + +export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } diff --git a/@/hooks/use-toast.ts b/@/hooks/use-toast.ts new file mode 100644 index 0000000..02e111d --- /dev/null +++ b/@/hooks/use-toast.ts @@ -0,0 +1,194 @@ +"use client" + +// 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_SAFE_INTEGER + 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 } diff --git a/app/client/convertUtcDateToLocalDateString.ts b/app/client/convertUtcDateToLocalDateString.ts new file mode 100644 index 0000000..eb44744 --- /dev/null +++ b/app/client/convertUtcDateToLocalDateString.ts @@ -0,0 +1,15 @@ +/** + * @description + * Takes a UTC date formatted string and returns the date as a date string + * formatted as the clients local time. + */ + +export const convertUtcDateToLocalDateString = ( + date: string | Date +): string => { + // const momentObject = convertUtcDateToLocalMomentObject(UTCDateString); + // return momentObject.toLocaleString(); + // return momentObject.local().format(DATE_FORMAT); + + return new Date(date).toLocaleString(); +}; diff --git a/app/client/fallbackImageSource.ts b/app/client/fallbackImageSource.ts new file mode 100644 index 0000000..0319323 --- /dev/null +++ b/app/client/fallbackImageSource.ts @@ -0,0 +1,2 @@ +export const fallbackImageSource = + "data:image/png;base64,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"; diff --git a/app/client/index.ts b/app/client/index.ts new file mode 100644 index 0000000..daef4ec --- /dev/null +++ b/app/client/index.ts @@ -0,0 +1,2 @@ +export * from "./convertUtcDateToLocalDateString"; +export * from "./fallbackImageSource"; diff --git a/app/components/CopyToClipboardButton.tsx b/app/components/CopyToClipboardButton.tsx new file mode 100644 index 0000000..ec10c8e --- /dev/null +++ b/app/components/CopyToClipboardButton.tsx @@ -0,0 +1,69 @@ +import { Copy, Check } from "lucide-react"; +import { useState } from "react"; +import { Button } from "@/components/ui/button"; +import { useToast } from "@/hooks/use-toast"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@/components/ui/tooltip"; + +interface CopyToClipboardButtonProps { + stringToCopy: string; +} + +const CopyToClipboardButton = ({ + stringToCopy, +}: CopyToClipboardButtonProps) => { + const { toast } = useToast(); + const [copied, setCopied] = useState(false); + + const handleCopyToClipboard = async () => { + try { + await navigator.clipboard.writeText(stringToCopy); + setCopied(true); + + toast({ + title: "Copied to clipboard", + description: stringToCopy, + duration: 2000, + }); + + // Reset copied state after 2 seconds + setTimeout(() => setCopied(false), 2000); + } catch (err) { + toast({ + title: "Failed to copy", + description: "Please try again", + variant: "destructive", + }); + } + }; + + return ( + + + + + + +

Copy to clipboard

+
+
+
+ ); +}; + +export default CopyToClipboardButton; diff --git a/app/hooks/useLoggedInUser.ts b/app/hooks/useLoggedInUser.ts index b65cbb5..96dfed7 100644 --- a/app/hooks/useLoggedInUser.ts +++ b/app/hooks/useLoggedInUser.ts @@ -1,8 +1,18 @@ import { useRouteLoaderData } from "@remix-run/react"; -import type { loader as rootLoader } from "~/root"; // Make sure to export the loader type from root.tsx +import type { loader as RootLoaderData } from "~/root"; // Make sure to export the loader type from root.tsx -export const useLoggedInUser = () => { - const rootData = useRouteLoaderData("root"); +export function useOptionalUser() { + const data = useRouteLoaderData("root"); + return data?.userData ?? null; +} - return rootData?.userData; -}; +export function useLoggedInUser() { + const maybeUser = useOptionalUser(); + if (!maybeUser) { + console.log( + "No user found in root loader, but user is required by useLoggedInUser. If user is optional, try useOptionalUser instead." + ); + } + + return maybeUser; +} diff --git a/app/root.tsx b/app/root.tsx index 437c34f..746f101 100644 --- a/app/root.tsx +++ b/app/root.tsx @@ -8,11 +8,12 @@ import { ScrollRestoration, useLoaderData, useLocation, - useRouteLoaderData, + // useRouteLoaderData, } from "@remix-run/react"; import { LoaderFunctionArgs } from "@remix-run/node"; import { Analytics } from "@vercel/analytics/react"; -import { Toaster, toast as showToast } from "sonner"; +// import { Toaster, toast as showToast } from "sonner"; +import { Toaster } from "@/components/ui/toaster"; import NavigationSidebar from "components/NavigationSidebar"; import { csrf } from "./utils/csrf.server"; import { getEnv } from "./utils/env.server"; @@ -20,7 +21,7 @@ import { combineHeaders } from "./utils/combineHeaders"; import { AuthenticityTokenProvider } from "remix-utils/csrf/react"; import { HoneypotProvider } from "remix-utils/honeypot/react"; import { honeypot } from "utils/honeypot.server"; -import { getToast, type Toast } from "utils/toast.server"; +// import { getToast, type Toast } from "utils/toast.server"; import { getLoggedInUserGoogleSSOData } from "./server"; import { GeneralErrorBoundary } from "./components/GeneralErrorBoundary"; import { @@ -37,7 +38,7 @@ export async function loader({ request }: LoaderFunctionArgs) { const [csrfToken, csrfCookieHeader] = await csrf.commitToken(request); const honeyProps = honeypot.getInputProps(); - const { toast, headers: toastHeaders } = await getToast(request); + // const { toast, headers: toastHeaders } = await getToast(request); // let user; // try { @@ -61,7 +62,7 @@ export async function loader({ request }: LoaderFunctionArgs) { return json( { userData, - toast, + // toast, ENV: getEnv(), csrfToken, honeyProps, @@ -69,8 +70,8 @@ export async function loader({ request }: LoaderFunctionArgs) { }, { headers: combineHeaders( - csrfCookieHeader ? { "set-cookie": csrfCookieHeader } : null, - toastHeaders + csrfCookieHeader ? { "set-cookie": csrfCookieHeader } : null + // toastHeaders // sessionCookieHeader ), } @@ -115,7 +116,7 @@ function Document({ __html: `window.ENV = ${JSON.stringify(env)}`, }} /> */} - + {/* */} @@ -125,7 +126,7 @@ function Document({ } export function Layout({ children }: { children: React.ReactNode }) { - const loaderData = useRouteLoaderData("root"); + // const loaderData = useRouteLoaderData("root"); // console.log(loaderData); const location = useLocation(); const isHome = location.pathname === "/"; @@ -135,9 +136,9 @@ export function Layout({ children }: { children: React.ReactNode }) { {/* */} {!isHome && } {children} - {loaderData && loaderData.toast ? ( + {/* {loaderData && loaderData.toast ? ( - ) : null} + ) : null} */} ); @@ -150,20 +151,21 @@ export default function App() { + ); } -function ShowToast({ toast }: { toast: Toast }) { - const { id, type, title, description } = toast; - React.useEffect(() => { - setTimeout(() => { - showToast[type](title, { id, description }); - }, 0); - }, [description, id, title, type]); - return null; -} +// function ShowToast({ toast }: { toast: Toast }) { +// const { id, type, title, description } = toast; +// React.useEffect(() => { +// setTimeout(() => { +// showToast[type](title, { id, description }); +// }, 0); +// }, [description, id, title, type]); +// return null; +// } export function ErrorBoundary() { return ; diff --git a/package-lock.json b/package-lock.json index f1c7634..da90fc1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,12 +12,15 @@ "@conform-to/zod": "^1.1.5", "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.19.0", - "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", + "@radix-ui/react-tooltip": "^1.1.3", "@remix-run/express": "^2.11.2", "@remix-run/node": "^2.11.2", "@remix-run/react": "^2.11.2", @@ -2792,11 +2795,11 @@ } }, "node_modules/@radix-ui/react-avatar": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.0.tgz", - "integrity": "sha512-Q/PbuSMk/vyAd/UoIShVGZ7StHHeRFYU7wXmi5GV+8cLXflZAEpHL/F697H1klrzxKXNtZ97vWiC0q3RKUH8UA==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.1.tgz", + "integrity": "sha512-eoOtThOmxeoizxpX6RiEsQZ2wj5r4+zoeqAwO0cBaFQGjJwIH3dIX0OCxNrCyrrdxG+vBweMETh3VziQG7c1kw==", "dependencies": { - "@radix-ui/react-context": "1.1.0", + "@radix-ui/react-context": "1.1.1", "@radix-ui/react-primitive": "2.0.0", "@radix-ui/react-use-callback-ref": "1.1.0", "@radix-ui/react-use-layout-effect": "1.1.0" @@ -2816,6 +2819,20 @@ } } }, + "node_modules/@radix-ui/react-avatar/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-collection": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.0.tgz", @@ -3461,6 +3478,310 @@ } } }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz", + "integrity": "sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-roving-focus": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-toast/-/react-toast-1.2.2.tgz", + "integrity": "sha512-Z6pqSzmAP/bFJoqMAston4eSNa+ud44NSZTiZUmUen+IOZ5nBY8kzuU5WDBVyFXPtcW6yUalOHsxM/BP6Sv8ww==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-collection": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-toast/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.3.tgz", + "integrity": "sha512-Z4w1FIS0BqVFI2c1jZvb/uDVJijJjJ2ZMuPV81oVgTZ7g3BZxobplnMVvXtFWgtozdvYJ+MFWtwkM5S2HnAong==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.0", + "@radix-ui/react-portal": "1.1.2", + "@radix-ui/react-presence": "1.1.1", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-slot": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz", + "integrity": "sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.0", + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-portal": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.2.tgz", + "integrity": "sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip/node_modules/@radix-ui/react-presence": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.1.tgz", + "integrity": "sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/react-use-callback-ref": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", @@ -3557,6 +3878,28 @@ } } }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.0.tgz", + "integrity": "sha512-N8MDZqtgCgG5S3aV60INAB475osJousYpZ4cTJ2cFbMpdHS5Y6loLTH8LPtkj2QN0x93J30HT/M3qJXM0+lyeQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, "node_modules/@radix-ui/rect": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", diff --git a/package.json b/package.json index 33f8d59..0afa915 100644 --- a/package.json +++ b/package.json @@ -17,12 +17,15 @@ "@conform-to/zod": "^1.1.5", "@paralleldrive/cuid2": "^2.2.2", "@prisma/client": "^5.19.0", - "@radix-ui/react-avatar": "^1.1.0", + "@radix-ui/react-avatar": "^1.1.1", "@radix-ui/react-dialog": "^1.1.2", "@radix-ui/react-dropdown-menu": "^2.1.1", "@radix-ui/react-label": "^2.1.0", "@radix-ui/react-popover": "^1.1.1", "@radix-ui/react-scroll-area": "^1.2.0", + "@radix-ui/react-tabs": "^1.1.1", + "@radix-ui/react-toast": "^1.2.2", + "@radix-ui/react-tooltip": "^1.1.3", "@remix-run/express": "^2.11.2", "@remix-run/node": "^2.11.2", "@remix-run/react": "^2.11.2", From 05f84f1367fbdd1fbe2a2849f267f9226e4e7aad Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 16:44:45 -0800 Subject: [PATCH 02/13] renamed explore page --- app/pages/ExplorePage.tsx | 2 +- app/routes/{explore.tsx => explore._index.tsx} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename app/routes/{explore.tsx => explore._index.tsx} (100%) diff --git a/app/pages/ExplorePage.tsx b/app/pages/ExplorePage.tsx index a80d676..061e705 100644 --- a/app/pages/ExplorePage.tsx +++ b/app/pages/ExplorePage.tsx @@ -2,7 +2,7 @@ import React from "react"; import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; import { ErrorList } from "components/ErrorList"; import ImageV2 from "components/ImageV2"; -import { type ExplorePageLoader } from "../routes/explore"; +import { type ExplorePageLoader } from "../routes/explore._index"; import { Search as MagnifyingGlassIcon } from "lucide-react"; import { PageContainer } from "~/components"; diff --git a/app/routes/explore.tsx b/app/routes/explore._index.tsx similarity index 100% rename from app/routes/explore.tsx rename to app/routes/explore._index.tsx From 246d46193280d9d0f04799d33b3ec872e337f2fd Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 17:35:32 -0800 Subject: [PATCH 03/13] setup `explore$imageID` page --- app/components/ImageV2.tsx | 38 +++--- app/components/index.ts | 2 + app/pages/ImageDetailsPage.tsx | 199 ++++++++++++++++++++++++++++++++ app/routes/explore.$imageId.tsx | 49 ++++++++ 4 files changed, 269 insertions(+), 19 deletions(-) create mode 100644 app/pages/ImageDetailsPage.tsx create mode 100644 app/routes/explore.$imageId.tsx diff --git a/app/components/ImageV2.tsx b/app/components/ImageV2.tsx index 5513563..05db79d 100644 --- a/app/components/ImageV2.tsx +++ b/app/components/ImageV2.tsx @@ -1,34 +1,34 @@ -// import { useLocation, useNavigate } from "@remix-run/react"; +import { useLocation, useNavigate } from "@remix-run/react"; import { ImageTagType } from "server/getImages"; const ImageV2 = ({ imageData }: { imageData: ImageTagType }) => { - // const navigate = useNavigate(); - // const location = useLocation(); + const navigate = useNavigate(); + const location = useLocation(); - // const handleImageClick = () => { - // // redirect user to explore.$imageId page when image is clicked - // // (this is the page that shows the image modal) - // navigate( - // `${ - // location.search - // ? `${imageData.id}${location.search}` - // : `${imageData.id}` - // }`, - // ); - // }; + const handleImageClick = () => { + // redirect user to explore.$imageId page when image is clicked + // (this is the page that shows the image modal) + navigate( + `${ + location.search + ? `${imageData.id}${location.search}` + : `${imageData.id}` + }` + ); + }; return ( -
handleImageClick()} +
+ ); }; diff --git a/app/components/index.ts b/app/components/index.ts index b19e7f0..ed5ab81 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -3,3 +3,5 @@ export * from "./UserAvatarButton"; export * from "./LogOutButton"; export * from "./PageContainer"; export * from "./CreatePageForm"; +export { default as CopyToClipboardButton } from "./CopyToClipboardButton"; +export { default as ImageV2 } from "./ImageV2"; \ No newline at end of file diff --git a/app/pages/ImageDetailsPage.tsx b/app/pages/ImageDetailsPage.tsx new file mode 100644 index 0000000..f4ff155 --- /dev/null +++ b/app/pages/ImageDetailsPage.tsx @@ -0,0 +1,199 @@ +import React from "react"; +import { useLoggedInUser } from "~/hooks"; +import { useLoaderData } from "@remix-run/react"; +import { ExplorePageImageLoader } from "~/routes/explore.$imageId"; +import { convertUtcDateToLocalDateString, fallbackImageSource } from "~/client"; +import { Dialog, DialogContent } from "@/components/ui/dialog"; +import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; +import { Avatar, AvatarFallback } from "@/components/ui/avatar"; +import { Heart, MessageCircle, Send, User, X, Bookmark } from "lucide-react"; + +interface ImageDetailsPageProps { + onClose: () => void; +} + +const ImageDetailsPage = ({ onClose }: ImageDetailsPageProps) => { + const { data: imageData } = useLoaderData(); + const userData = useLoggedInUser(); + const isUserLoggedIn = Boolean(userData); + const formRef = React.useRef(null); + const isLoadingFetcher = false; + + const handleCommentFormSubmit = (event: React.FormEvent) => { + event.preventDefault(); + formRef.current?.reset(); + }; + + if (!imageData) return null; + + return ( + <> + {/* Dark overlay */} +
+ + + e.preventDefault()} + > + + +
+ {/* Left side - Image */} +
+
+ {imageData.prompt { + e.currentTarget.src = fallbackImageSource; + }} + /> +
+
+ + {/* Right side - Details */} +
+ {/* Header */} +
+ +
+ + {/* Comments Section - Scrollable */} +
+
+ {/* Original Post */} +
+ + + + + +
+
+ + {imageData.user?.username} + + + {imageData.prompt} + +
+
+ + {convertUtcDateToLocalDateString( + imageData.createdAt! + )} + +
+
+
+ + {/* Comments placeholder */} +

+ No comments yet. +

+
+
+ + {/* Actions Bar - Fixed at bottom */} +
+
+
+
+ + + +
+ +
+ +
+

0 likes

+

+ {convertUtcDateToLocalDateString(imageData.createdAt!)} +

+
+ + {/* Comment Input - Now separated with better contrast */} + {isUserLoggedIn && ( +
+ + +
+ )} +
+
+
+
+
+
+ + ); +}; +export default ImageDetailsPage; diff --git a/app/routes/explore.$imageId.tsx b/app/routes/explore.$imageId.tsx new file mode 100644 index 0000000..21c838a --- /dev/null +++ b/app/routes/explore.$imageId.tsx @@ -0,0 +1,49 @@ +import { + type LoaderFunctionArgs, + json, + type SerializeFrom, + MetaFunction, +} from "@remix-run/node"; +import { useNavigate } from "@remix-run/react"; +import { getImage } from "~/server"; +import { invariantResponse } from "~/utils"; +import ImageDetailsPage from "~/pages/ImageDetailsPage"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; + +export const meta: MetaFunction = () => { + return [{ title: "Explore AI Generated Images" }]; +}; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const imageId = params.imageId || ""; + invariantResponse(imageId, "Image does not exist"); + + const image = await getImage(imageId); + return json({ data: image }); +}; + +export type ExplorePageImageLoader = SerializeFrom; + +export default function Index() { + const navigate = useNavigate(); + + const handleCloseModal = () => { + navigate("/explore"); + }; + + + return ; +} + +export const ErrorBoundary = () => { + return ( +

You do not have permission

, + 404: ({ params }) => ( +

Image with id: "{params.imageId}" does not exist

+ ), + }} + /> + ); +}; From e9baa2179c5d50f5a7356f308ed0625f7264c6d0 Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 17:47:00 -0800 Subject: [PATCH 04/13] Added tabs section --- app/pages/ImageDetailsPage.tsx | 163 +++++++++++++++++++++++---------- 1 file changed, 113 insertions(+), 50 deletions(-) diff --git a/app/pages/ImageDetailsPage.tsx b/app/pages/ImageDetailsPage.tsx index f4ff155..15e380c 100644 --- a/app/pages/ImageDetailsPage.tsx +++ b/app/pages/ImageDetailsPage.tsx @@ -7,7 +7,17 @@ import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { Heart, MessageCircle, Send, User, X, Bookmark } from "lucide-react"; +import { + Heart, + MessageCircle, + Send, + User, + X, + Bookmark, + Info, +} from "lucide-react"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { CopyToClipboardButton } from "~/components"; interface ImageDetailsPageProps { onClose: () => void; @@ -65,62 +75,115 @@ const ImageDetailsPage = ({ onClose }: ImageDetailsPageProps) => {
{/* Header */}
- -
- - {/* Comments Section - Scrollable */} -
-
- {/* Original Post */} -
- +
+
+ -
-
- - {imageData.user?.username} - - - {imageData.prompt} - -
-
- - {convertUtcDateToLocalDateString( - imageData.createdAt! - )} - -
+
- - {/* Comments placeholder */} -

- No comments yet. -

+ {/* Tabs Section */} + + + + + Comments + + + + Info + + + + +
+ {/* Original Post */} + {/*
+ + + + + +
+
+ + {imageData.user?.username} + + + {imageData.prompt} + +
+
+ + {convertUtcDateToLocalDateString( + imageData.createdAt! + )} + +
+
+
*/} + + {imageData.comments && imageData.comments.length ? ( + imageData.comments.map((comment) => ( +
{comment.message}
+ )) + ) : ( +

+ No comments yet. +

+ )} +
+
+ + +
+
+

Engine Model

+

{imageData.model}

+
+ +
+

Style Preset

+

{imageData.stylePreset}

+
+ +
+

Prompt

+
+

{imageData.prompt}

+ +
+
+
+
+
+ {/* Actions Bar - Fixed at bottom */}
@@ -133,13 +196,13 @@ const ImageDetailsPage = ({ onClose }: ImageDetailsPageProps) => { > - + */} + */} +
+ +
+
+ {userImages.length}{" "} + posts +
+
+ 0{" "} + followers +
+
+ 0{" "} + following +
+
+ + {/*
+

{userData.name}

+

+ {userData.bio || "No bio yet."} +

+
*/} +
+
+ + {/* Tabs */} + + + + + POSTS + + + + {/* Scrollable Content */} + + +
+
+ + ); +} diff --git a/app/routes/profile.$userId.tsx b/app/routes/profile.$userId.tsx new file mode 100644 index 0000000..5850943 --- /dev/null +++ b/app/routes/profile.$userId.tsx @@ -0,0 +1,66 @@ +import { + type LoaderFunctionArgs, + json, + SerializeFrom, + MetaFunction, +} from "@remix-run/node"; +import UserProfilePage from "~/pages/UserProfilePage"; +import { getUserDataByUserId } from "~/server"; +import { loader as UserLoaderData } from "../root"; +import { invariantResponse } from "~/utils/invariantResponse"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; + +export const meta: MetaFunction< + typeof loader, + { root: typeof UserLoaderData } +> = ({ params, matches }) => { + // TODO: Use user's username instead of userId so we can dynamically store it in our meta tag + const userId = params.userId; + + // Incase our Profile loader ever fails, we can get logged in user data from root + const userMatch = matches.find((match) => match.id === "root"); + const username = + userMatch?.data.data?.username || userMatch?.data.data?.name || userId; + + return [ + { title: `${username} | Profile` }, + { + name: "description", + content: `Checkout ${username}'s AI generated images`, + }, + ]; +}; + +export const loader = async ({ request, params }: LoaderFunctionArgs) => { + const userId = params.userId || ""; + invariantResponse(userId, "UserId does not exist"); + + const searchParams = new URL(request.url).searchParams; + const currentPage = Math.max(Number(searchParams.get("page") || 1), 1); + const pageSize = Number(searchParams.get("page_size")) || 250; + + const data = await getUserDataByUserId(userId, currentPage, pageSize); + + invariantResponse(data.user, "User does not exist"); + + return json(data); +}; + +export type UserProfilePageLoader = SerializeFrom; + +export default function Index() { + return ; +} + +export const ErrorBoundary = () => { + return ( +

You do not have permission

, + 404: ({ params }) => ( +

User with id: "{params.userId}" does not exist

+ ), + }} + /> + ); +}; diff --git a/app/server/index.ts b/app/server/index.ts index accc984..de766f3 100644 --- a/app/server/index.ts +++ b/app/server/index.ts @@ -15,3 +15,4 @@ export * from "./getImage"; export * from "./getImageBase64"; export * from "./getImageBlobFromS3"; export * from "./updateUserCredits"; +export * from "./getUserDataByUserId"; \ No newline at end of file From 31a5074b009d68f823aa16c766f0771100d02d5f Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 20:46:33 -0800 Subject: [PATCH 08/13] added error boundary page container --- app/routes/_index.tsx | 10 +++++++++ app/routes/create.tsx | 7 ++++++- app/routes/explore.$imageId.tsx | 11 ++++++---- app/routes/explore._index.tsx | 10 +++++++++ app/routes/login.tsx | 9 ++++++++ app/routes/p.$imageId.tsx | 37 +++++++++++++++++++++++++++++++++ app/routes/profile.$userId.tsx | 21 +++++++++++-------- app/routes/set.$setId.tsx | 10 +++++++++ 8 files changed, 101 insertions(+), 14 deletions(-) create mode 100644 app/routes/p.$imageId.tsx diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 6eb3b79..2fd174e 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,5 +1,15 @@ import LandingPage from "pages/LandingPage"; +import { PageContainer } from "~/components"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; export default function Index() { return ; } + +export const ErrorBoundary = () => { + return ( + + + + ); +}; diff --git a/app/routes/create.tsx b/app/routes/create.tsx index 485a9ac..1cefc78 100644 --- a/app/routes/create.tsx +++ b/app/routes/create.tsx @@ -10,6 +10,7 @@ import { requireUserLogin } from "~/services"; import CreatePage from "~/pages/CreatePage"; import { createNewImages, updateUserCredits } from "~/server"; import { z } from "zod"; +import { PageContainer } from "~/components"; export const meta: MetaFunction = () => { return [{ title: "Create AI Generated Images" }]; @@ -247,5 +248,9 @@ export default function Index() { } export function ErrorBoundary() { - return ; + return ( + + + + ); } diff --git a/app/routes/explore.$imageId.tsx b/app/routes/explore.$imageId.tsx index 3e59809..21e32ad 100644 --- a/app/routes/explore.$imageId.tsx +++ b/app/routes/explore.$imageId.tsx @@ -9,6 +9,7 @@ import { getImage } from "~/server"; import { invariantResponse } from "~/utils"; import ExploreImageDetailsPage from "~/pages/ExploreImageDetailsPage"; import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { PageContainer } from "~/components"; export const meta: MetaFunction = () => { return [{ title: "Explore AI Generated Images" }]; @@ -36,13 +37,15 @@ export default function Index() { export const ErrorBoundary = () => { return ( -

You do not have permission

, + +

You do not have permission

, 404: ({ params }) => (

Image with id: "{params.imageId}" does not exist

), }} - /> + /> +
); }; diff --git a/app/routes/explore._index.tsx b/app/routes/explore._index.tsx index 676d917..855039d 100644 --- a/app/routes/explore._index.tsx +++ b/app/routes/explore._index.tsx @@ -2,6 +2,8 @@ import { type LoaderFunctionArgs, json, MetaFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import ExplorePage from "pages/ExplorePage"; import { getImages } from "server/getImages"; +import { PageContainer } from "~/components"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; import { requireUserLogin } from "~/services"; export const meta: MetaFunction = () => { @@ -31,3 +33,11 @@ export default function Index() { ); } + +export const ErrorBoundary = () => { + return ( + + + + ); +}; diff --git a/app/routes/login.tsx b/app/routes/login.tsx index d173fff..05d8e97 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -6,6 +6,7 @@ import { Card } from "@/components/ui/card"; import { json, MetaFunction } from "@remix-run/react"; // import { getSupabaseWithSessionAndHeaders } from "~/services/supabase.server"; import { requireAnonymous } from "~/services"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; export const meta: MetaFunction = () => { return [{ title: "Login to Pixel Studio AI" }]; @@ -43,3 +44,11 @@ export default function Index() { ); } + +export const ErrorBoundary = () => { + return ( + + + + ); +}; diff --git a/app/routes/p.$imageId.tsx b/app/routes/p.$imageId.tsx new file mode 100644 index 0000000..e2dac81 --- /dev/null +++ b/app/routes/p.$imageId.tsx @@ -0,0 +1,37 @@ +import { + type LoaderFunctionArgs, + json, + type SerializeFrom, + MetaFunction, +} from "@remix-run/node"; +import { getImage } from "~/server"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { invariantResponse } from "~/utils"; + +export const meta: MetaFunction = () => { + return [{ title: "Image Details Page" }]; +}; + +export const loader = async ({ params }: LoaderFunctionArgs) => { + const imageId = params.imageId || ""; + invariantResponse(imageId, "Image does not exist"); + + const image = await getImage(imageId); + + return json({ data: image }); +}; + +export type ImageDetailsPageImageLoader = SerializeFrom; + +export const ErrorBoundary = () => { + return ( +

You do not have permission

, + 404: ({ params }) => ( +

Image with id: "{params.imageId}" does not exist

+ ), + }} + /> + ); +}; diff --git a/app/routes/profile.$userId.tsx b/app/routes/profile.$userId.tsx index 5850943..c9d96ad 100644 --- a/app/routes/profile.$userId.tsx +++ b/app/routes/profile.$userId.tsx @@ -9,6 +9,7 @@ import { getUserDataByUserId } from "~/server"; import { loader as UserLoaderData } from "../root"; import { invariantResponse } from "~/utils/invariantResponse"; import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { PageContainer } from "~/components"; export const meta: MetaFunction< typeof loader, @@ -46,7 +47,7 @@ export const loader = async ({ request, params }: LoaderFunctionArgs) => { return json(data); }; -export type UserProfilePageLoader = SerializeFrom; +export type UserProfilePageLoader = typeof loader; export default function Index() { return ; @@ -54,13 +55,15 @@ export default function Index() { export const ErrorBoundary = () => { return ( -

You do not have permission

, - 404: ({ params }) => ( -

User with id: "{params.userId}" does not exist

- ), - }} - /> + +

You do not have permission

, + 404: ({ params }) => ( +

User with id: "{params.userId}" does not exist

+ ), + }} + /> +
); }; diff --git a/app/routes/set.$setId.tsx b/app/routes/set.$setId.tsx index ce86c9f..fef6660 100644 --- a/app/routes/set.$setId.tsx +++ b/app/routes/set.$setId.tsx @@ -4,6 +4,8 @@ import { MetaFunction, redirect, } from "@remix-run/node"; +import { PageContainer } from "~/components"; +import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; import SetDetailsPage from "~/pages/SetDetailsPage"; import { getSet } from "~/server/getSet"; import { requireUserLogin } from "~/services"; @@ -34,3 +36,11 @@ export default function Index() { ); } + +export const ErrorBoundary = () => { + return ( + + + + ); +}; From 5c9f82eb6e091854ec7604ad66005dd15295e30e Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 20:48:55 -0800 Subject: [PATCH 09/13] updated rendering images --- app/components/ImageV2.tsx | 60 +++++++++++++++++++---------------- app/pages/ExplorePage.tsx | 3 +- app/pages/UserProfilePage.tsx | 38 ++++++++-------------- 3 files changed, 48 insertions(+), 53 deletions(-) diff --git a/app/components/ImageV2.tsx b/app/components/ImageV2.tsx index 05db79d..e6d4c52 100644 --- a/app/components/ImageV2.tsx +++ b/app/components/ImageV2.tsx @@ -1,34 +1,40 @@ -import { useLocation, useNavigate } from "@remix-run/react"; +import { Link } from "@remix-run/react"; +// import { Heart, MessageCircle } from "lucide-react"; import { ImageTagType } from "server/getImages"; -const ImageV2 = ({ imageData }: { imageData: ImageTagType }) => { - const navigate = useNavigate(); - const location = useLocation(); - - const handleImageClick = () => { - // redirect user to explore.$imageId page when image is clicked - // (this is the page that shows the image modal) - navigate( - `${ - location.search - ? `${imageData.id}${location.search}` - : `${imageData.id}` - }` - ); - }; +const ImageV2 = ({ + imageData, + onClickRedirectTo = "", +}: { + imageData: ImageTagType; + onClickRedirectTo?: string; +}) => { + const redirectTo = onClickRedirectTo || `/explore/${imageData.id}`; return ( - +
+ + {imageData.prompt} + + {/* Hover Overlay */} + {/*
+
+
+ + 0 +
+
+ + 0 +
+
+
*/} +
); }; diff --git a/app/pages/ExplorePage.tsx b/app/pages/ExplorePage.tsx index 061e705..c775ecb 100644 --- a/app/pages/ExplorePage.tsx +++ b/app/pages/ExplorePage.tsx @@ -13,6 +13,7 @@ import { PageContainer } from "~/components"; const ExplorePage = () => { const loaderData = useLoaderData(); + const images = loaderData.data.images || []; const [searchParams] = useSearchParams(); const initialSearchTerm = searchParams.get("q") || ""; @@ -52,7 +53,7 @@ const ExplorePage = () => {
{/* highlight on hover reference: https://www.hyperui.dev/blog/highlight-hover-effect-with-tailwindcss */} {images.length > 0 ? ( -
    +
      {images.map( (image) => // This removes Typescript error: "image is possibly 'null'." diff --git a/app/pages/UserProfilePage.tsx b/app/pages/UserProfilePage.tsx index 59252e5..ee407ff 100644 --- a/app/pages/UserProfilePage.tsx +++ b/app/pages/UserProfilePage.tsx @@ -142,34 +142,22 @@ export default function UserProfilePage() {
      {userImages.length > 0 ? ( -
      +
        {userImages.map((image) => ( -
        + {/*
        -
        - -
        - {/* Hover Overlay */} -
        -
        -
        - - 0 -
        -
        - - 0 -
        -
        -
        -
        + className="relative aspect-square group cursor-pointer hover:opacity-90 transition-opacity" */} + {/* > */} + {/*
        */} + + {/*
        */} + ))} -
        +
      ) : (

      No Posts Yet

      From bc867c4f99257fcb11af6b6976ce0e1ee0289e17 Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 20:51:32 -0800 Subject: [PATCH 10/13] updated handle close --- app/routes/explore.$imageId.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/app/routes/explore.$imageId.tsx b/app/routes/explore.$imageId.tsx index 21e32ad..8973489 100644 --- a/app/routes/explore.$imageId.tsx +++ b/app/routes/explore.$imageId.tsx @@ -29,7 +29,11 @@ export default function Index() { const navigate = useNavigate(); const handleCloseModal = () => { - navigate("/explore"); + if (window.history.length > 2) { + navigate(-1); + } else { + navigate("/explore"); // Fallback if there's no history + } }; return ; From 525f0249d8438965dbb851b7a52c26157adc0e86 Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 21:17:31 -0800 Subject: [PATCH 11/13] updated close image dialog --- app/pages/ExploreImageDetailsPage.tsx | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/app/pages/ExploreImageDetailsPage.tsx b/app/pages/ExploreImageDetailsPage.tsx index 8fecb6f..7e8eb08 100644 --- a/app/pages/ExploreImageDetailsPage.tsx +++ b/app/pages/ExploreImageDetailsPage.tsx @@ -12,7 +12,6 @@ import { MessageCircle, Send, User, - X, Bookmark, Info, } from "lucide-react"; @@ -45,18 +44,9 @@ const ExploreImageDetailsPage = ({ onClose }: ExploreImageDetailsPageProps) => { e.preventDefault()} > - -
      {/* Left side - Image */}
      From e62ae81d0fc14bfa3b019807aa2251d8f77fb1f5 Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 21:23:58 -0800 Subject: [PATCH 12/13] updated imports --- app/components/GeneralErrorBoundary.tsx | 4 +++- app/components/{ImageV2.tsx => ImageCard.tsx} | 4 ++-- app/components/index.ts | 3 ++- app/pages/ExploreImageDetailsPage.tsx | 2 +- app/pages/ExplorePage.tsx | 6 ++---- app/pages/UserProfilePage.tsx | 7 +++---- app/routes/_index.tsx | 3 +-- app/routes/create.tsx | 3 +-- app/routes/explore.$imageId.tsx | 3 +-- app/routes/explore._index.tsx | 3 +-- app/routes/login.tsx | 3 +-- app/routes/p.$imageId.tsx | 2 +- app/routes/profile.$userId.tsx | 4 +--- app/routes/set.$setId.tsx | 3 +-- 14 files changed, 21 insertions(+), 29 deletions(-) rename app/components/{ImageV2.tsx => ImageCard.tsx} (96%) diff --git a/app/components/GeneralErrorBoundary.tsx b/app/components/GeneralErrorBoundary.tsx index e24f985..6b1c1de 100644 --- a/app/components/GeneralErrorBoundary.tsx +++ b/app/components/GeneralErrorBoundary.tsx @@ -24,7 +24,7 @@ type StatusHandler = (info: { params: Record; }) => JSX.Element | null; -export function GeneralErrorBoundary({ +function GeneralErrorBoundary({ defaultStatusHandler = ({ error }) => (

      {error.status} {getErrorMessage(error.data)} @@ -55,3 +55,5 @@ export function GeneralErrorBoundary({

      ); } + +export default GeneralErrorBoundary; diff --git a/app/components/ImageV2.tsx b/app/components/ImageCard.tsx similarity index 96% rename from app/components/ImageV2.tsx rename to app/components/ImageCard.tsx index e6d4c52..a9e2d74 100644 --- a/app/components/ImageV2.tsx +++ b/app/components/ImageCard.tsx @@ -2,7 +2,7 @@ import { Link } from "@remix-run/react"; // import { Heart, MessageCircle } from "lucide-react"; import { ImageTagType } from "server/getImages"; -const ImageV2 = ({ +const ImageCard = ({ imageData, onClickRedirectTo = "", }: { @@ -38,4 +38,4 @@ const ImageV2 = ({ ); }; -export default ImageV2; +export default ImageCard; diff --git a/app/components/index.ts b/app/components/index.ts index ed5ab81..3647080 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -4,4 +4,5 @@ export * from "./LogOutButton"; export * from "./PageContainer"; export * from "./CreatePageForm"; export { default as CopyToClipboardButton } from "./CopyToClipboardButton"; -export { default as ImageV2 } from "./ImageV2"; \ No newline at end of file +export { default as ImageCard } from "./ImageCard"; +export { default as GeneralErrorBoundary } from "./GeneralErrorBoundary"; diff --git a/app/pages/ExploreImageDetailsPage.tsx b/app/pages/ExploreImageDetailsPage.tsx index 7e8eb08..d3c29cd 100644 --- a/app/pages/ExploreImageDetailsPage.tsx +++ b/app/pages/ExploreImageDetailsPage.tsx @@ -6,6 +6,7 @@ import { convertUtcDateToLocalDateString, fallbackImageSource } from "~/client"; import { Dialog, DialogContent } from "@/components/ui/dialog"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Heart, @@ -15,7 +16,6 @@ import { Bookmark, Info, } from "lucide-react"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { CopyToClipboardButton } from "~/components"; interface ExploreImageDetailsPageProps { diff --git a/app/pages/ExplorePage.tsx b/app/pages/ExplorePage.tsx index c775ecb..cd4c930 100644 --- a/app/pages/ExplorePage.tsx +++ b/app/pages/ExplorePage.tsx @@ -1,10 +1,8 @@ import React from "react"; import { Form, useLoaderData, useSearchParams } from "@remix-run/react"; -import { ErrorList } from "components/ErrorList"; -import ImageV2 from "components/ImageV2"; import { type ExplorePageLoader } from "../routes/explore._index"; import { Search as MagnifyingGlassIcon } from "lucide-react"; -import { PageContainer } from "~/components"; +import { PageContainer, ImageCard, ErrorList } from "~/components"; /** * @@ -59,7 +57,7 @@ const ExplorePage = () => { // This removes Typescript error: "image is possibly 'null'." image && (
    • - +
    • ) )} diff --git a/app/pages/UserProfilePage.tsx b/app/pages/UserProfilePage.tsx index ee407ff..0a43459 100644 --- a/app/pages/UserProfilePage.tsx +++ b/app/pages/UserProfilePage.tsx @@ -3,10 +3,9 @@ import { Avatar, AvatarFallback } from "@/components/ui/avatar"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; // import { Grid, Settings, User } from "lucide-react"; -import { PageContainer } from "~/components"; +import { PageContainer, ImageCard } from "~/components"; import type { UserProfilePageLoader } from "~/routes/profile.$userId"; -import ImageV2 from "~/components/ImageV2"; -import { Grid, Settings, User, Heart, MessageCircle } from "lucide-react"; +import { Grid, User } from "lucide-react"; const UserDoesNotExist = () => { return ( @@ -150,7 +149,7 @@ export default function UserProfilePage() { className="relative aspect-square group cursor-pointer hover:opacity-90 transition-opacity" */} {/* > */} {/*
      */} - diff --git a/app/routes/_index.tsx b/app/routes/_index.tsx index 2fd174e..e3a62c1 100644 --- a/app/routes/_index.tsx +++ b/app/routes/_index.tsx @@ -1,6 +1,5 @@ import LandingPage from "pages/LandingPage"; -import { PageContainer } from "~/components"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { PageContainer, GeneralErrorBoundary } from "~/components"; export default function Index() { return ; diff --git a/app/routes/create.tsx b/app/routes/create.tsx index 1cefc78..841a7b7 100644 --- a/app/routes/create.tsx +++ b/app/routes/create.tsx @@ -5,12 +5,11 @@ import { MetaFunction, redirect, } from "@remix-run/node"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; import { requireUserLogin } from "~/services"; import CreatePage from "~/pages/CreatePage"; import { createNewImages, updateUserCredits } from "~/server"; import { z } from "zod"; -import { PageContainer } from "~/components"; +import { PageContainer, GeneralErrorBoundary } from "~/components"; export const meta: MetaFunction = () => { return [{ title: "Create AI Generated Images" }]; diff --git a/app/routes/explore.$imageId.tsx b/app/routes/explore.$imageId.tsx index 8973489..f7d5d78 100644 --- a/app/routes/explore.$imageId.tsx +++ b/app/routes/explore.$imageId.tsx @@ -8,8 +8,7 @@ import { useNavigate } from "@remix-run/react"; import { getImage } from "~/server"; import { invariantResponse } from "~/utils"; import ExploreImageDetailsPage from "~/pages/ExploreImageDetailsPage"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; -import { PageContainer } from "~/components"; +import { GeneralErrorBoundary, PageContainer } from "~/components"; export const meta: MetaFunction = () => { return [{ title: "Explore AI Generated Images" }]; diff --git a/app/routes/explore._index.tsx b/app/routes/explore._index.tsx index 855039d..fca0389 100644 --- a/app/routes/explore._index.tsx +++ b/app/routes/explore._index.tsx @@ -2,8 +2,7 @@ import { type LoaderFunctionArgs, json, MetaFunction } from "@remix-run/node"; import { useLoaderData } from "@remix-run/react"; import ExplorePage from "pages/ExplorePage"; import { getImages } from "server/getImages"; -import { PageContainer } from "~/components"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { PageContainer, GeneralErrorBoundary } from "~/components"; import { requireUserLogin } from "~/services"; export const meta: MetaFunction = () => { diff --git a/app/routes/login.tsx b/app/routes/login.tsx index 05d8e97..414a1c6 100644 --- a/app/routes/login.tsx +++ b/app/routes/login.tsx @@ -1,4 +1,4 @@ -import { PageContainer } from "~/components"; +import { PageContainer, GeneralErrorBoundary } from "~/components"; import PixelStudioIcon from "components/PixelStudioIcon"; import type { LoaderFunctionArgs } from "@remix-run/node"; import GoogleLoginButton from "../components/GoogleLoginButton"; @@ -6,7 +6,6 @@ import { Card } from "@/components/ui/card"; import { json, MetaFunction } from "@remix-run/react"; // import { getSupabaseWithSessionAndHeaders } from "~/services/supabase.server"; import { requireAnonymous } from "~/services"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; export const meta: MetaFunction = () => { return [{ title: "Login to Pixel Studio AI" }]; diff --git a/app/routes/p.$imageId.tsx b/app/routes/p.$imageId.tsx index e2dac81..8c59467 100644 --- a/app/routes/p.$imageId.tsx +++ b/app/routes/p.$imageId.tsx @@ -5,7 +5,7 @@ import { MetaFunction, } from "@remix-run/node"; import { getImage } from "~/server"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { GeneralErrorBoundary } from "~/components"; import { invariantResponse } from "~/utils"; export const meta: MetaFunction = () => { diff --git a/app/routes/profile.$userId.tsx b/app/routes/profile.$userId.tsx index c9d96ad..4448681 100644 --- a/app/routes/profile.$userId.tsx +++ b/app/routes/profile.$userId.tsx @@ -1,15 +1,13 @@ import { type LoaderFunctionArgs, json, - SerializeFrom, MetaFunction, } from "@remix-run/node"; import UserProfilePage from "~/pages/UserProfilePage"; import { getUserDataByUserId } from "~/server"; import { loader as UserLoaderData } from "../root"; import { invariantResponse } from "~/utils/invariantResponse"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; -import { PageContainer } from "~/components"; +import { PageContainer, GeneralErrorBoundary } from "~/components"; export const meta: MetaFunction< typeof loader, diff --git a/app/routes/set.$setId.tsx b/app/routes/set.$setId.tsx index fef6660..e783249 100644 --- a/app/routes/set.$setId.tsx +++ b/app/routes/set.$setId.tsx @@ -4,8 +4,7 @@ import { MetaFunction, redirect, } from "@remix-run/node"; -import { PageContainer } from "~/components"; -import { GeneralErrorBoundary } from "~/components/GeneralErrorBoundary"; +import { PageContainer, GeneralErrorBoundary } from "~/components"; import SetDetailsPage from "~/pages/SetDetailsPage"; import { getSet } from "~/server/getSet"; import { requireUserLogin } from "~/services"; From aa0d124d8c9fb24d142abd2f3ba31009328d89ca Mon Sep 17 00:00:00 2001 From: Kevin Reber Date: Sat, 9 Nov 2024 21:28:37 -0800 Subject: [PATCH 13/13] updated impiorts --- app/components/GeneralErrorBoundary.tsx | 8 ++++---- app/components/index.ts | 2 +- app/pages/ExploreImageDetailsPage.tsx | 17 ++++++++--------- 3 files changed, 13 insertions(+), 14 deletions(-) diff --git a/app/components/GeneralErrorBoundary.tsx b/app/components/GeneralErrorBoundary.tsx index 6b1c1de..1bc3627 100644 --- a/app/components/GeneralErrorBoundary.tsx +++ b/app/components/GeneralErrorBoundary.tsx @@ -24,7 +24,7 @@ type StatusHandler = (info: { params: Record; }) => JSX.Element | null; -function GeneralErrorBoundary({ +const GeneralErrorBoundary = ({ defaultStatusHandler = ({ error }) => (

      {error.status} {getErrorMessage(error.data)} @@ -36,7 +36,7 @@ function GeneralErrorBoundary({ defaultStatusHandler?: StatusHandler; statusHandlers?: Record; unexpectedErrorHandler?: (error: unknown) => JSX.Element | null; -}) { +}) => { const error = useRouteError(); const params = useParams(); @@ -54,6 +54,6 @@ function GeneralErrorBoundary({ : unexpectedErrorHandler(error)}

      ); -} +}; -export default GeneralErrorBoundary; +export { GeneralErrorBoundary }; diff --git a/app/components/index.ts b/app/components/index.ts index 3647080..5170363 100644 --- a/app/components/index.ts +++ b/app/components/index.ts @@ -5,4 +5,4 @@ export * from "./PageContainer"; export * from "./CreatePageForm"; export { default as CopyToClipboardButton } from "./CopyToClipboardButton"; export { default as ImageCard } from "./ImageCard"; -export { default as GeneralErrorBoundary } from "./GeneralErrorBoundary"; +export * from "./GeneralErrorBoundary"; diff --git a/app/pages/ExploreImageDetailsPage.tsx b/app/pages/ExploreImageDetailsPage.tsx index d3c29cd..2fe1af7 100644 --- a/app/pages/ExploreImageDetailsPage.tsx +++ b/app/pages/ExploreImageDetailsPage.tsx @@ -8,14 +8,7 @@ import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import { Avatar, AvatarFallback } from "@/components/ui/avatar"; -import { - Heart, - MessageCircle, - Send, - User, - Bookmark, - Info, -} from "lucide-react"; +import { Heart, MessageCircle, Send, User, Bookmark, Info } from "lucide-react"; import { CopyToClipboardButton } from "~/components"; interface ExploreImageDetailsPageProps { @@ -164,7 +157,13 @@ const ExploreImageDetailsPage = ({ onClose }: ExploreImageDetailsPageProps) => {

      Style Preset

      -

      {imageData.stylePreset}

      + {imageData.stylePreset ? ( +

      + {imageData.stylePreset} +

      + ) : ( +

      none

      + )}