aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components/new/document-modal/content
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/components/new/document-modal/content')
-rw-r--r--apps/web/components/new/document-modal/content/google-doc.tsx66
-rw-r--r--apps/web/components/new/document-modal/content/image-preview.tsx56
-rw-r--r--apps/web/components/new/document-modal/content/index.tsx143
-rw-r--r--apps/web/components/new/document-modal/content/text-editor-content.tsx91
4 files changed, 356 insertions, 0 deletions
diff --git a/apps/web/components/new/document-modal/content/google-doc.tsx b/apps/web/components/new/document-modal/content/google-doc.tsx
new file mode 100644
index 00000000..562bac12
--- /dev/null
+++ b/apps/web/components/new/document-modal/content/google-doc.tsx
@@ -0,0 +1,66 @@
+"use client"
+
+import { useState } from "react"
+import { Loader2 } from "lucide-react"
+import {
+ extractGoogleDocId,
+ getGoogleEmbedUrl,
+} from "@/lib/url-helpers"
+
+interface GoogleDocViewerProps {
+ url: string | null | undefined
+ customId: string | null | undefined
+ type: "google_doc" | "google_sheet" | "google_slide"
+}
+
+export function GoogleDocViewer({ url, customId, type }: GoogleDocViewerProps) {
+ const [loading, setLoading] = useState(true)
+ const [error, setError] = useState<string | null>(null)
+
+ const docId = customId ?? (url ? extractGoogleDocId(url) : null)
+
+ if (!docId) {
+ return (
+ <div className="flex items-center justify-center h-full text-gray-400">
+ Unable to load document - no document ID found
+ </div>
+ )
+ }
+
+ const embedUrl = getGoogleEmbedUrl(docId, type)
+
+ const typeLabels = {
+ google_doc: "Google Doc",
+ google_sheet: "Google Sheet",
+ google_slide: "Google Slides",
+ }
+
+ return (
+ <div className="flex flex-col h-full w-full overflow-hidden">
+ {loading && (
+ <div className="absolute inset-0 flex items-center justify-center bg-[#14161A] z-10">
+ <div className="flex items-center gap-2 text-gray-400">
+ <Loader2 className="w-5 h-5 animate-spin" />
+ <span>Loading {typeLabels[type]}...</span>
+ </div>
+ </div>
+ )}
+ {error && (
+ <div className="flex items-center justify-center h-full text-red-400">
+ {error}
+ </div>
+ )}
+ <iframe
+ src={embedUrl}
+ className="flex-1 w-full h-full border-0 rounded-[14px]"
+ onLoad={() => setLoading(false)}
+ onError={() => {
+ setLoading(false)
+ setError("Failed to load document")
+ }}
+ allow="autoplay"
+ title={typeLabels[type]}
+ />
+ </div>
+ )
+}
diff --git a/apps/web/components/new/document-modal/content/image-preview.tsx b/apps/web/components/new/document-modal/content/image-preview.tsx
new file mode 100644
index 00000000..31d5c1c0
--- /dev/null
+++ b/apps/web/components/new/document-modal/content/image-preview.tsx
@@ -0,0 +1,56 @@
+"use client"
+
+import { useState } from "react"
+import { cn } from "@lib/utils"
+
+interface ImagePreviewProps {
+ url: string
+ title?: string | null
+}
+
+export function ImagePreview({ url, title }: ImagePreviewProps) {
+ const [imageError, setImageError] = useState(false)
+ const [isLoading, setIsLoading] = useState(true)
+
+ if (imageError || !url) {
+ return (
+ <div className="flex items-center justify-center h-full text-[#737373]">
+ <p>Failed to load image</p>
+ </div>
+ )
+ }
+
+ return (
+ <div className="relative w-full h-full overflow-hidden flex items-center justify-center bg-[#0B1017]">
+ {isLoading && (
+ <div className="absolute inset-0 bg-cover bg-center animate-pulse">
+ <div className="w-full h-full bg-[#1B1F24]" />
+ </div>
+ )}
+ <div
+ className="absolute inset-0 bg-cover bg-center"
+ style={{
+ backgroundImage: `url(${url})`,
+ filter: "blur(100px)",
+ transform: "scale(1.1)",
+ opacity: isLoading ? 0.5 : 1,
+ }}
+ />
+ <div className="absolute inset-0 bg-black/30" />
+ <img
+ src={url}
+ alt={title || "Image preview"}
+ className={cn(
+ "relative max-w-full max-h-full w-auto h-auto object-contain z-10",
+ isLoading && "opacity-0",
+ )}
+ onError={() => {
+ setImageError(true)
+ setIsLoading(false)
+ }}
+ onLoad={() => setIsLoading(false)}
+ loading="lazy"
+ />
+ </div>
+ )
+}
diff --git a/apps/web/components/new/document-modal/content/index.tsx b/apps/web/components/new/document-modal/content/index.tsx
new file mode 100644
index 00000000..c06bc550
--- /dev/null
+++ b/apps/web/components/new/document-modal/content/index.tsx
@@ -0,0 +1,143 @@
+"use client"
+
+import type { DocumentsWithMemoriesResponseSchema } from "@repo/validation/api"
+import type { z } from "zod"
+import dynamic from "next/dynamic"
+import { isTwitterUrl } from "@/lib/url-helpers"
+import { ImagePreview } from "./image-preview"
+import { TweetContent } from "./tweet"
+import { NotionDoc } from "./notion-doc"
+import { YoutubeVideo } from "./yt-video"
+import { WebPageContent } from "./web-page"
+import { TextEditorContent } from "./text-editor-content"
+import { GoogleDocViewer } from "./google-doc"
+import type { TextEditorProps } from "./text-editor-content"
+
+export type { TextEditorProps }
+
+const PdfViewer = dynamic(
+ () => import("./pdf").then((mod) => ({ default: mod.PdfViewer })),
+ {
+ ssr: false,
+ loading: () => (
+ <div className="flex items-center justify-center h-full text-gray-400">
+ Loading PDF viewer...
+ </div>
+ ),
+ },
+) as typeof import("./pdf").PdfViewer
+
+type DocumentsResponse = z.infer<typeof DocumentsWithMemoriesResponseSchema>
+type DocumentWithMemories = DocumentsResponse["documents"][0]
+
+interface DocumentContentProps {
+ document: DocumentWithMemories | null
+ textEditorProps: TextEditorProps
+}
+
+type ContentType =
+ | "image"
+ | "tweet"
+ | "text"
+ | "pdf"
+ | "notion"
+ | "youtube"
+ | "webpage"
+ | "google_doc"
+ | "google_sheet"
+ | "google_slide"
+ | null
+
+function getContentType(document: DocumentWithMemories | null): ContentType {
+ if (!document) return null
+
+ const isImage =
+ document.type === "image" ||
+ document.metadata?.mimeType?.toString().startsWith("image/")
+
+ if (isImage && document.url) return "image"
+ if (
+ document.type === "tweet" ||
+ (document.url && isTwitterUrl(document.url))
+ )
+ return "tweet"
+ if (document.type === "text") return "text"
+ if (document.type === "pdf") return "pdf"
+ if (document.type === "notion_doc") return "notion"
+ if (document.type === "google_doc") return "google_doc"
+ if (document.type === "google_sheet") return "google_sheet"
+ if (document.type === "google_slide") return "google_slide"
+ if (document.url?.includes("youtube.com")) return "youtube"
+ if (document.type === "webpage") return "webpage"
+
+ return null
+}
+
+export function DocumentContent({
+ document,
+ textEditorProps,
+}: DocumentContentProps) {
+ const contentType = getContentType(document)
+
+ if (!document || !contentType) return null
+
+ switch (contentType) {
+ case "image":
+ return (
+ <ImagePreview url={document.url ?? ""} title={document.title} />
+ )
+
+ case "tweet":
+ return (
+ <TweetContent
+ url={document.url}
+ tweetMetadata={document.metadata?.sm_internal_twitter_metadata}
+ />
+ )
+
+ case "text":
+ return <TextEditorContent {...textEditorProps} />
+
+ case "pdf":
+ return <PdfViewer url={document.url} />
+
+ case "notion":
+ return <NotionDoc content={document.content ?? ""} />
+
+ case "youtube":
+ return <YoutubeVideo url={document.url ?? ""} />
+
+ case "webpage":
+ return <WebPageContent content={document.content ?? ""} />
+
+ case "google_doc":
+ return (
+ <GoogleDocViewer
+ url={document.url}
+ customId={document.customId}
+ type="google_doc"
+ />
+ )
+
+ case "google_sheet":
+ return (
+ <GoogleDocViewer
+ url={document.url}
+ customId={document.customId}
+ type="google_sheet"
+ />
+ )
+
+ case "google_slide":
+ return (
+ <GoogleDocViewer
+ url={document.url}
+ customId={document.customId}
+ type="google_slide"
+ />
+ )
+
+ default:
+ return null
+ }
+}
diff --git a/apps/web/components/new/document-modal/content/text-editor-content.tsx b/apps/web/components/new/document-modal/content/text-editor-content.tsx
new file mode 100644
index 00000000..98c5beb4
--- /dev/null
+++ b/apps/web/components/new/document-modal/content/text-editor-content.tsx
@@ -0,0 +1,91 @@
+"use client"
+
+import { cn } from "@lib/utils"
+import { dmSansClassName } from "@/lib/fonts"
+import { TextEditor } from "../../text-editor"
+import { motion, AnimatePresence } from "motion/react"
+import { Button } from "@repo/ui/components/button"
+import { Loader2 } from "lucide-react"
+
+export interface TextEditorProps {
+ documentId: string
+ editorResetNonce: number
+ initialEditorContent: string | undefined
+ hasUnsavedChanges: boolean
+ isSaving: boolean
+ onContentChange: (content: string) => void
+ onSave: () => void
+ onReset: () => void
+}
+
+export function TextEditorContent({
+ documentId,
+ editorResetNonce,
+ initialEditorContent,
+ hasUnsavedChanges,
+ isSaving,
+ onContentChange,
+ onSave,
+ onReset,
+}: TextEditorProps) {
+ return (
+ <>
+ <div className="p-4 overflow-y-auto flex-1 scrollbar-thin">
+ <TextEditor
+ key={`${documentId}-${editorResetNonce}`}
+ content={initialEditorContent}
+ onContentChange={onContentChange}
+ onSubmit={onSave}
+ />
+ </div>
+ <AnimatePresence>
+ {hasUnsavedChanges && (
+ <motion.div
+ initial={{ opacity: 0, y: 20 }}
+ animate={{ opacity: 1, y: 0 }}
+ exit={{ opacity: 0, y: 20 }}
+ transition={{ duration: 0.2 }}
+ className="absolute bottom-4 left-1/2 -translate-x-1/2 flex items-center gap-3 px-4 py-2 bg-[#1B1F24] rounded-full shadow-[0_4px_20px_rgba(0,0,0,0.4),inset_1px_1px_1px_rgba(255,255,255,0.1)]"
+ >
+ <span className="text-sm text-[#737373]">Unsaved changes</span>
+ <Button
+ variant="ghost"
+ size="sm"
+ onClick={onReset}
+ disabled={isSaving}
+ className="text-[#737373]/80 hover:text-white rounded-full px-3"
+ >
+ Cancel
+ </Button>
+ <Button
+ variant="insideOut"
+ size="sm"
+ onClick={onSave}
+ disabled={isSaving}
+ className="hover:text-white rounded-full px-4"
+ >
+ {isSaving ? (
+ <>
+ <Loader2 className="size-4 animate-spin mr-1" />
+ Saving...
+ </>
+ ) : (
+ <>
+ Save
+ <span
+ className={cn(
+ "bg-[#21212180] border border-[#73737333] text-[#737373] rounded-sm px-1 py-0.5 text-[10px] flex items-center justify-center",
+ dmSansClassName(),
+ )}
+ >
+ ⌘+Enter
+ </span>
+ </>
+ )}
+ </Button>
+ </motion.div>
+ )}
+ </AnimatePresence>
+ </>
+ )
+}