ubiquitous-invention/apps/web/components/views/embed/embed-view.tsx
Randall Stillwell a508ece6e7 feat: Full project management application scaffold
Complete architecture for a ClickUp/Notion/Miro-class project management app:

- Turborepo monorepo with Next.js 15, TypeScript, PostgreSQL (Drizzle ORM)
- Object-centered database schema (everything is an Object: tasks, projects, docs, whiteboards)
- NextAuth v5 authentication with credentials + OAuth providers
- tRPC v11 API layer with full CRUD for objects, properties, relations, templates, search
- Three-panel UI: collapsible sidebar, center content area, push-in right panel
- Purple/teal theme with light/dark mode via Shadcn/ui + Tailwind CSS
- Multiple views: List, Kanban board (dnd-kit), Table (spreadsheet), Embedded iframe
- TipTap rich text editor with slash commands, custom blocks (callout, toggle, mention, embed, divider), AI block
- Real-time collaboration via Yjs + Hocuspocus with presence/cursors
- tldraw whiteboard with custom shape cards (task, document, project)
- MCP server exposing all app data/tools for AI agents
- AI chat panel, editor AI slash commands, Cmd+K command palette
- Template system with built-in templates (Bug Report, Meeting Notes, Sprint)
- Full-text search with result highlighting
- Docker Compose for full-stack deployment (web + collab + postgres + redis)

Made-with: Cursor
2026-03-26 22:39:16 -05:00

312 lines
9.4 KiB
TypeScript

"use client";
import * as React from "react";
import {
AlertTriangle,
ExternalLink,
Loader2,
Pencil,
RefreshCw,
} from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import type { ViewConfig } from "@/lib/hooks/use-view-data";
import { cn } from "@/lib/utils";
/** Allowed schemes for embed src; rejects javascript:, data:, etc. */
export function sanitizeEmbedUrl(raw: string): string | null {
const trimmed = raw.trim();
if (!trimmed) return null;
let parsed: URL;
try {
parsed = new URL(trimmed);
} catch {
return null;
}
if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
return null;
}
// href is serialized safely for iframe src (no HTML/script injection)
return parsed.href;
}
const EXAMPLE_SERVICES = [
"Figma",
"Google Docs",
"Miro",
"Loom",
"YouTube",
] as const;
export interface EmbedViewProps {
config: ViewConfig & { embedUrl?: string };
className?: string;
}
export function EmbedView({ config, className }: EmbedViewProps) {
const [sessionUrl, setSessionUrl] = React.useState<string | null>(null);
const [draft, setDraft] = React.useState("");
const [validationError, setValidationError] = React.useState<string | null>(
null,
);
const effectiveUrl = config.embedUrl ?? sessionUrl;
React.useEffect(() => {
if (config.embedUrl) {
setDraft(config.embedUrl);
}
}, [config.embedUrl]);
const handlePreview = () => {
setValidationError(null);
const safe = sanitizeEmbedUrl(draft);
if (!safe) {
setValidationError(
"Enter a valid URL starting with http:// or https://",
);
return;
}
setSessionUrl(safe);
};
const handleChangeUrl = () => {
setSessionUrl(null);
setDraft(effectiveUrl ?? "");
setValidationError(null);
};
if (!effectiveUrl) {
return (
<div
className={cn(
"flex min-h-[320px] flex-1 flex-col items-center justify-center gap-6 p-8",
className,
)}
>
<div className="w-full max-w-md space-y-4 text-center">
<div className="space-y-1">
<h2 className="text-lg font-semibold tracking-tight">
Embed a page
</h2>
<p className="text-sm text-muted-foreground">
Paste a link to show it inside this view. Works best with tools
that allow embedding.
</p>
</div>
<div className="flex flex-col gap-2 sm:flex-row sm:items-stretch">
<Input
type="url"
inputMode="url"
autoComplete="url"
placeholder="Paste a URL to embed..."
value={draft}
onChange={(e) => {
setDraft(e.target.value);
setValidationError(null);
}}
onKeyDown={(e) => {
if (e.key === "Enter") handlePreview();
}}
className="text-left"
aria-invalid={!!validationError}
/>
<Button type="button" className="shrink-0 sm:w-auto" onClick={handlePreview}>
Preview
</Button>
</div>
{validationError ? (
<p className="text-left text-sm text-destructive" role="alert">
{validationError}
</p>
) : null}
<div className="space-y-2">
<p className="text-xs font-medium text-muted-foreground">
Often works well with
</p>
<div className="flex flex-wrap justify-center gap-1.5">
{EXAMPLE_SERVICES.map((label) => (
<Badge key={label} variant="outline" className="font-normal">
{label}
</Badge>
))}
</div>
</div>
<div
className="flex items-start gap-2 rounded-md border border-amber-500/30 bg-amber-500/5 px-3 py-2 text-left text-xs text-muted-foreground"
role="note"
>
<AlertTriangle
className="mt-0.5 size-3.5 shrink-0 text-amber-600 dark:text-amber-500"
aria-hidden
/>
<span>
Many sites block embedding in iframes for security. If you see a
blank page or an error, the site may not allow this viewuse{" "}
<span className="font-medium text-foreground">Open in new tab</span>{" "}
after loading.
</span>
</div>
</div>
</div>
);
}
return (
<EmbedFrame
key={effectiveUrl}
url={effectiveUrl}
onChangeUrl={handleChangeUrl}
className={className}
/>
);
}
interface EmbedFrameProps {
url: string;
onChangeUrl: () => void;
className?: string;
}
function EmbedFrame({ url, onChangeUrl, className }: EmbedFrameProps) {
const [loadState, setLoadState] = React.useState<
"loading" | "loaded" | "error"
>("loading");
const [reloadToken, setReloadToken] = React.useState(0);
const safeUrl = sanitizeEmbedUrl(url);
const iframeSrc = safeUrl ?? "";
React.useEffect(() => {
if (!safeUrl) {
setLoadState("error");
return;
}
setLoadState("loading");
}, [safeUrl, reloadToken]);
const handleRefresh = () => {
setLoadState("loading");
setReloadToken((x) => x + 1);
};
const displayUrl = safeUrl ?? url;
const truncated =
displayUrl.length > 64 ? `${displayUrl.slice(0, 61)}` : displayUrl;
return (
<div
className={cn(
"flex min-h-0 flex-1 flex-col overflow-hidden rounded-md border border-border bg-background",
className,
)}
>
<div
className="flex h-10 shrink-0 items-center gap-2 border-b border-border bg-muted/30 px-2"
role="toolbar"
aria-label="Embed controls"
>
<span
className="min-w-0 flex-1 truncate px-1 text-xs text-muted-foreground"
title={displayUrl}
>
{truncated}
</span>
<div className="flex shrink-0 items-center gap-0.5">
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={handleRefresh}
disabled={!safeUrl}
aria-label="Refresh embed"
>
<RefreshCw className="size-4" />
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2"
asChild
>
<a
href={safeUrl || "#"}
target="_blank"
rel="noopener noreferrer"
aria-label="Open in new tab"
>
<ExternalLink className="size-4" />
</a>
</Button>
<Button
type="button"
variant="ghost"
size="sm"
className="h-8 px-2"
onClick={onChangeUrl}
aria-label="Change URL"
>
<Pencil className="size-4" />
</Button>
</div>
</div>
<div className="relative min-h-0 flex-1 bg-muted/20">
{!safeUrl ? (
<div className="flex h-full min-h-[200px] items-center justify-center p-6 text-center text-sm text-destructive">
Invalid or unsupported URL. Use Change URL to enter an http(s)
link.
</div>
) : (
<>
{loadState === "loading" ? (
<div
className="absolute inset-0 z-10 flex items-center justify-center bg-background/80 backdrop-blur-[1px]"
aria-live="polite"
aria-busy="true"
>
<Loader2 className="size-8 animate-spin text-primary" aria-hidden />
<span className="sr-only">Loading embed</span>
</div>
) : null}
{loadState === "error" ? (
<div className="absolute inset-0 z-10 flex flex-col items-center justify-center gap-3 bg-background p-6 text-center">
<p className="text-sm font-medium text-destructive">
Could not load this page in the embed.
</p>
<p className="max-w-sm text-xs text-muted-foreground">
The URL may be invalid, blocked, or the site may forbid
framing. Try opening in a new tab or a different URL.
</p>
<div className="flex flex-wrap justify-center gap-2">
<Button type="button" size="sm" onClick={handleRefresh}>
Try again
</Button>
<Button type="button" size="sm" variant="outline" asChild>
<a href={safeUrl} target="_blank" rel="noopener noreferrer">
Open in new tab
</a>
</Button>
</div>
</div>
) : null}
{loadState !== "error" ? (
<iframe
key={`${iframeSrc}-${reloadToken}`}
title="Embedded content"
src={iframeSrc}
className="size-full min-h-[400px] border-0"
referrerPolicy="no-referrer-when-downgrade"
onLoad={() => setLoadState("loaded")}
onError={() => setLoadState("error")}
/>
) : null}
</>
)}
</div>
</div>
);
}