From edc59c022cad81a3db9d9cc8da3b0f7d4f98b94a Mon Sep 17 00:00:00 2001 From: urjitc <135136842+urjitc@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:19:10 +0000 Subject: [PATCH 1/4] Revert "fix: include flashcard zoom in default and rehydrated data" This reverts commit c692ffde0828f63187f778aa562b13818b8dba31. --- src/lib/workspace-state/item-helpers.ts | 1 - src/lib/workspace-state/state.ts | 1 - src/lib/workspace/workspace-item-model.ts | 7 ------- 3 files changed, 9 deletions(-) diff --git a/src/lib/workspace-state/item-helpers.ts b/src/lib/workspace-state/item-helpers.ts index e4e9756a..da498d7d 100644 --- a/src/lib/workspace-state/item-helpers.ts +++ b/src/lib/workspace-state/item-helpers.ts @@ -17,7 +17,6 @@ export function defaultDataFor(type: CardType): ItemData { back: "", }, ], - zoom: 1, } as FlashcardData; case "folder": return {} as FolderData; diff --git a/src/lib/workspace-state/state.ts b/src/lib/workspace-state/state.ts index 316a0411..9616d18f 100644 --- a/src/lib/workspace-state/state.ts +++ b/src/lib/workspace-state/state.ts @@ -40,7 +40,6 @@ export function defaultDataFor(type: CardType): ItemData { case "flashcard": return { cards: [], - zoom: 1, } as FlashcardData; case "folder": return {} as FolderData; diff --git a/src/lib/workspace/workspace-item-model.ts b/src/lib/workspace/workspace-item-model.ts index d91f13ae..e6304775 100644 --- a/src/lib/workspace/workspace-item-model.ts +++ b/src/lib/workspace/workspace-item-model.ts @@ -180,17 +180,10 @@ export function rehydrateWorkspaceItemData( const structuredData = isRecord(contentRecord.structuredData) ? contentRecord.structuredData : {}; - const z = - typeof structuredData.zoom === "number" && - structuredData.zoom >= 0.5 && - structuredData.zoom <= 2.5 - ? structuredData.zoom - : 1; merged = { cards: Array.isArray(structuredData.cards) ? (structuredData.cards as FlashcardItem[]) : [], - zoom: z, } satisfies FlashcardData; break; } From 576b9edf8e0cda6a8b6c1b681054e2b788406228 Mon Sep 17 00:00:00 2001 From: urjitc <135136842+urjitc@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:19:13 +0000 Subject: [PATCH 2/4] Revert "Feat: Flashcard edit modal with Tiptap and zoom controls (#442)" This reverts commit 2d2e83f2fe650eee0ce304a2e893f7ca11620059. --- src/app/dashboard/page.tsx | 1 - .../workspace-canvas/FlashcardContent.tsx | 174 ++---------------- .../FlashcardWorkspaceCard.tsx | 112 +---------- .../workspace-canvas/WorkspaceContent.tsx | 8 +- .../workspace-canvas/WorkspaceGrid.tsx | 9 +- .../workspace-canvas/WorkspaceSection.tsx | 8 +- src/lib/workspace-state/item-data-schemas.ts | 1 - 7 files changed, 18 insertions(+), 295 deletions(-) diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 1cb4d4d0..3e47f854 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -371,7 +371,6 @@ function DashboardContent({ state={state} addItem={operations.createItem} updateItem={operations.updateItem} - updateItemData={operations.updateItemData} deleteItem={operations.deleteItem} updateAllItems={operations.updateAllItems} isChatMaximized={isChatMaximized} diff --git a/src/components/workspace-canvas/FlashcardContent.tsx b/src/components/workspace-canvas/FlashcardContent.tsx index 082ed22b..b1c4cb10 100644 --- a/src/components/workspace-canvas/FlashcardContent.tsx +++ b/src/components/workspace-canvas/FlashcardContent.tsx @@ -1,101 +1,37 @@ "use client"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Check } from "lucide-react"; -import { DocumentEditor } from "@/components/editor/DocumentEditor"; +import { useMemo } from "react"; import type { Item, ItemData, FlashcardData, } from "@/lib/workspace-state/types"; import { StreamdownMarkdown } from "@/components/ui/streamdown-markdown"; -import { cn } from "@/lib/utils"; interface FlashcardContentProps { item: Item; onUpdateData: (updater: (prev: ItemData) => ItemData) => void; } -function FlashcardSideEditable({ +function FlashcardSidePreview({ title, markdown, emptyLabel, - isEditing, - onStartEditing, - onStopEditing, - onChange, - cardName, }: { title: string; markdown: string; emptyLabel: string; - isEditing: boolean; - onStartEditing: () => void; - onStopEditing: () => void; - onChange: (markdown: string) => void; - cardName?: string; }) { - const containerRef = useRef(null); - - useEffect(() => { - if (!isEditing) return; - const el = containerRef.current; - if (!el) return; - - const handler = (e: KeyboardEvent) => { - if (e.key !== "Escape") return; - e.preventDefault(); - e.stopPropagation(); - onStopEditing(); - }; - - el.addEventListener("keydown", handler); - return () => el.removeEventListener("keydown", handler); - }, [isEditing, onStopEditing]); - return ( -
-
-
- {title} -
- {isEditing && ( - - )} +
+
+ {title}
{ - if (isEditing) return; - const target = e.target as HTMLElement; - if (target.closest("a")) return; - onStartEditing(); - }} > - {isEditing ? ( - onChange(nextMarkdown)} - /> - ) : !markdown.trim() ? ( + {!markdown.trim() ? (
{emptyLabel}
@@ -111,73 +47,9 @@ function FlashcardSideEditable({ ); } -export function FlashcardContent({ item, onUpdateData }: FlashcardContentProps) { +export function FlashcardContent({ item }: FlashcardContentProps) { const flashcardData = item.data as FlashcardData; const cards = useMemo(() => flashcardData.cards ?? [], [flashcardData.cards]); - const [editing, setEditing] = useState<{ - cardId: string; - side: "front" | "back"; - } | null>(null); - - // Local drafts override stale `card.front`/`card.back` while `onUpdateData` - // is debounced (~500ms). Without this, re-entering a side within the - // debounce window would mount the editor with the stale props value and - // silently drop the queued keystrokes when the user types again. Drafts - // also prevent the read-only preview from flickering back to old content - // between Done and flush. - const [drafts, setDrafts] = useState>({}); - const draftKey = (cardId: string, side: "front" | "back") => - `${cardId}:${side}`; - - const resolveMarkdown = useCallback( - (card: { id: string; front: string; back: string }, side: "front" | "back"): string => { - const draft = drafts[draftKey(card.id, side)]; - const source = side === "front" ? card.front : card.back; - return draft ?? source; - }, - [drafts], - ); - - // Drop a draft once the authoritative source (item.data) catches up, so - // future remote edits via realtime sync aren't masked by a stale local - // override. - useEffect(() => { - setDrafts((prev) => { - let changed = false; - const next: Record = {}; - for (const [key, value] of Object.entries(prev)) { - const [cardId, side] = key.split(":") as [string, "front" | "back"]; - const card = cards.find((c) => c.id === cardId); - if (!card) { - changed = true; - continue; - } - const source = side === "front" ? card.front : card.back; - if (source === value) { - changed = true; - continue; - } - next[key] = value; - } - return changed ? next : prev; - }); - }, [cards]); - - const handleChange = useCallback( - (cardId: string, side: "front" | "back", markdown: string) => { - setDrafts((prev) => ({ ...prev, [draftKey(cardId, side)]: markdown })); - onUpdateData((prev) => { - const prevFc = prev as FlashcardData; - return { - ...prevFc, - cards: (prevFc.cards ?? []).map((c) => - c.id === cardId ? { ...c, [side]: markdown } : c, - ), - } as FlashcardData; - }); - }, - [onUpdateData], - ); return (
@@ -201,38 +73,16 @@ export function FlashcardContent({ item, onUpdateData }: FlashcardContentProps)
- - setEditing({ cardId: card.id, side: "front" }) - } - onStopEditing={() => setEditing(null)} - onChange={(markdown) => - handleChange(card.id, "front", markdown) - } - cardName={item.name} /> - - setEditing({ cardId: card.id, side: "back" }) - } - onStopEditing={() => setEditing(null)} - onChange={(markdown) => - handleChange(card.id, "back", markdown) - } - cardName={item.name} />
diff --git a/src/components/workspace-canvas/FlashcardWorkspaceCard.tsx b/src/components/workspace-canvas/FlashcardWorkspaceCard.tsx index 1de720b6..047d3105 100644 --- a/src/components/workspace-canvas/FlashcardWorkspaceCard.tsx +++ b/src/components/workspace-canvas/FlashcardWorkspaceCard.tsx @@ -7,8 +7,6 @@ import { Trash2, CheckCircle2, Pencil, - ZoomIn, - ZoomOut, Palette, ChevronLeft, ChevronRight, @@ -19,7 +17,6 @@ import { PiMouseScrollFill, PiMouseScrollBold } from "react-icons/pi"; import { useTheme } from "next-themes"; import type { Item, - ItemData, FlashcardData, FlashcardItem, } from "@/lib/workspace-state/types"; @@ -73,16 +70,6 @@ interface FlashcardWorkspaceCardProps { workspaceIcon?: string | null; workspaceColor?: string | null; onUpdateItem: (itemId: string, updates: Partial) => void; - /** - * Optional updater-style data patcher. Preferred for patching individual - * fields inside `data` (e.g. `zoom`) because it composes with other - * in-flight `updateItemData` updaters instead of overwriting the whole - * blob the way `onUpdateItem({ data })` does. - */ - onUpdateItemData?: ( - itemId: string, - updater: (prev: ItemData) => ItemData, - ) => void; onDeleteItem: (itemId: string) => void; onOpenModal: (itemId: string) => void; onMoveItem?: (itemId: string, folderId: string | null) => void; // Callback to move item to folder @@ -96,15 +83,10 @@ const EMPTY_FLASHCARD_PLACEHOLDER: FlashcardItem = { back: "", }; -const ZOOM_MIN = 0.5; -const ZOOM_MAX = 2.5; -const ZOOM_STEP = 0.1; -const ZOOM_DEFAULT = 1; - /** Center all markdown blocks on the card; keep code blocks full-width and left-aligned. */ const FLASHCARD_STREAMDOWN_CLASS = cn( // Fluid type: size comes from the nearest [container-type:size] ancestor (card face) - "font-medium max-w-none text-center leading-[1.45] text-[length:calc(clamp(0.82rem,0.42rem+3cqmin,2.85rem)*var(--flashcard-zoom,1))]", + "font-medium max-w-none text-center leading-[1.45] text-[length:clamp(0.82rem,0.42rem+3cqmin,2.85rem)]", // globals.css fixes .streamdown-content at 0.875rem — inherit this wrapper’s fluid size instead "[&_.streamdown-content]:!text-inherit [&_.streamdown-content]:![font-size:1em]", "[&_.streamdown-content_p]:!text-inherit [&_.streamdown-content_li]:!text-inherit [&_.streamdown-content_td]:!text-inherit [&_.streamdown-content_th]:!text-inherit", @@ -147,7 +129,7 @@ const FlashcardSideMarkdownView = memo( >
{!markdown.trim() ? ( -
+
Ask the AI or click the pencil icon to add flashcards
) : ( @@ -173,7 +155,6 @@ export function FlashcardWorkspaceCard({ workspaceIcon, workspaceColor, onUpdateItem, - onUpdateItemData, onDeleteItem, onOpenModal, onMoveItem, @@ -196,9 +177,6 @@ export function FlashcardWorkspaceCard({ (state) => state.toggleItemScrollLocked, ); const flashcardData = item.data as FlashcardData; - const persistedZoom = - (flashcardData as FlashcardData & { zoom?: number }).zoom ?? ZOOM_DEFAULT; - const [localZoom, setLocalZoom] = useState(persistedZoom); // Navigation State const [currentIndex, setCurrentIndex] = useState(0); @@ -215,34 +193,6 @@ export function FlashcardWorkspaceCard({ } }, [cards.length, currentIndex]); - useEffect(() => { - setLocalZoom(persistedZoom); - }, [persistedZoom]); - - const clampZoom = (z: number) => - Math.min(ZOOM_MAX, Math.max(ZOOM_MIN, Math.round(z * 10) / 10)); - - const applyZoom = useCallback( - (next: number) => { - const clamped = clampZoom(next); - setLocalZoom(clamped); - if (onUpdateItemData) { - // Preferred path: compose with any in-flight `updateItemData` updaters - // for this item so we never clobber pending Tiptap edits. - onUpdateItemData(item.id, (prev) => ({ - ...(prev as FlashcardData), - zoom: clamped, - })); - return; - } - // Fallback (legacy wiring): full `data` replacement via updateItem. - onUpdateItem(item.id, { - data: { ...(item.data as FlashcardData), zoom: clamped }, - }); - }, - [item.id, item.data, onUpdateItem, onUpdateItemData], - ); - // Persist index change (optional debounce?) const handleIndexChange = useCallback((newIndex: number) => { // Don't persist on every click to avoid network spam, or do? @@ -535,7 +485,7 @@ export function FlashcardWorkspaceCard({
@@ -581,62 +531,6 @@ export function FlashcardWorkspaceCard({ -
- - - -
- diff --git a/src/components/workspace-canvas/WorkspaceHeader.tsx b/src/components/workspace-canvas/WorkspaceHeader.tsx index c481fae6..9eaede81 100644 --- a/src/components/workspace-canvas/WorkspaceHeader.tsx +++ b/src/components/workspace-canvas/WorkspaceHeader.tsx @@ -255,8 +255,6 @@ interface WorkspaceHeaderProps { // Workspace actions onOpenSettings?: () => void; onOpenShare?: () => void; - /** Open the raffle details popup (reopens the "Win a $50 Amazon gift card" dialog). */ - onShowRaffleDetails?: () => void; /** Item shown in the fullscreen workspace viewer (`openMode === "single"`) — breadcrumbs + header actions */ activeOpenWorkspaceItem?: Item | null; @@ -288,7 +286,6 @@ export function WorkspaceHeader({ onRenameFolder, onOpenSettings, onOpenShare, - onShowRaffleDetails, activeOpenWorkspaceItem = null, onCloseActiveItem, @@ -1243,10 +1240,10 @@ export function WorkspaceHeader({ Feedback - {isWorkspaceOwner && currentWorkspaceId && ( + {isWorkspaceOwner && currentWorkspaceId && onOpenShare && ( )} {onOpenShare && ( From f4280fcf2ee58e64a66f42db6fafa0a0b054a1eb Mon Sep 17 00:00:00 2001 From: urjitc <135136842+urjitc@users.noreply.github.com> Date: Thu, 23 Apr 2026 21:19:20 +0000 Subject: [PATCH 4/4] Revert "Merge pull request #441 from ThinkEx-OSS/capy/midterms-raffle-popup-workspace" This reverts commit 288f9fd84bc767bdf40cca7f30d0d6134cd6febc, reversing changes made to e3112344a5b375d094dc98ed996cc04856f003d4. --- drizzle/0003_famous_skaar.sql | 1 - drizzle/meta/0003_snapshot.json | 2083 ----------------- drizzle/meta/_journal.json | 7 - src/app/api/share-link/claim/route.ts | 52 +- .../api/workspaces/[id]/share-link/route.ts | 23 - src/app/dashboard/page.tsx | 12 - src/app/invite/claim/[token]/page.tsx | 45 +- src/components/modals/RafflePopup.tsx | 219 -- .../workspace-canvas/ShareCountBadge.tsx | 84 - .../workspace-canvas/WorkspaceHeader.tsx | 9 - src/lib/db/schema.ts | 2 +- 11 files changed, 34 insertions(+), 2503 deletions(-) delete mode 100644 drizzle/0003_famous_skaar.sql delete mode 100644 drizzle/meta/0003_snapshot.json delete mode 100644 src/components/modals/RafflePopup.tsx delete mode 100644 src/components/workspace-canvas/ShareCountBadge.tsx diff --git a/drizzle/0003_famous_skaar.sql b/drizzle/0003_famous_skaar.sql deleted file mode 100644 index 3511c3e8..00000000 --- a/drizzle/0003_famous_skaar.sql +++ /dev/null @@ -1 +0,0 @@ -ALTER TABLE "workspace_share_links" ADD COLUMN "claim_count" integer DEFAULT 0 NOT NULL; diff --git a/drizzle/meta/0003_snapshot.json b/drizzle/meta/0003_snapshot.json deleted file mode 100644 index 26089e8e..00000000 --- a/drizzle/meta/0003_snapshot.json +++ /dev/null @@ -1,2083 +0,0 @@ -{ - "id": "fe5effef-0bfd-4d84-84dc-571c0b80b2ae", - "prevId": "77a2e4b0-3244-4eda-8ffd-41ba7999b3e1", - "version": "7", - "dialect": "postgresql", - "tables": { - "public.account": { - "name": "account", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "account_id": { - "name": "account_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "provider_id": { - "name": "provider_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "access_token": { - "name": "access_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "refresh_token": { - "name": "refresh_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "id_token": { - "name": "id_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "access_token_expires_at": { - "name": "access_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "refresh_token_expires_at": { - "name": "refresh_token_expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": false - }, - "scope": { - "name": "scope", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "password": { - "name": "password", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_account_user_id": { - "name": "idx_account_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "account_user_id_user_id_fk": { - "name": "account_user_id_user_id_fk", - "tableFrom": "account", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chat_messages": { - "name": "chat_messages", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "thread_id": { - "name": "thread_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "message_id": { - "name": "message_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "parent_id": { - "name": "parent_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "format": { - "name": "format", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "content": { - "name": "content", - "type": "jsonb", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_chat_messages_thread": { - "name": "idx_chat_messages_thread", - "columns": [ - { - "expression": "thread_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_chat_messages_thread_created": { - "name": "idx_chat_messages_thread_created", - "columns": [ - { - "expression": "thread_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "created_at", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "timestamptz_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "chat_messages_thread_id_chat_threads_id_fk": { - "name": "chat_messages_thread_id_chat_threads_id_fk", - "tableFrom": "chat_messages", - "tableTo": "chat_threads", - "columnsFrom": ["thread_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "chat_messages_thread_message_key": { - "name": "chat_messages_thread_message_key", - "nullsNotDistinct": false, - "columns": ["thread_id", "message_id"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.chat_threads": { - "name": "chat_threads", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "title": { - "name": "title", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_archived": { - "name": "is_archived", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "external_id": { - "name": "external_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "last_message_at": { - "name": "last_message_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "head_message_id": { - "name": "head_message_id", - "type": "text", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_chat_threads_workspace": { - "name": "idx_chat_threads_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_chat_threads_user": { - "name": "idx_chat_threads_user", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_chat_threads_last_message": { - "name": "idx_chat_threads_last_message", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "last_message_at", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "timestamptz_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "chat_threads_workspace_id_workspaces_id_fk": { - "name": "chat_threads_workspace_id_workspaces_id_fk", - "tableFrom": "chat_threads", - "tableTo": "workspaces", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - }, - "chat_threads_user_id_user_id_fk": { - "name": "chat_threads_user_id_user_id_fk", - "tableFrom": "chat_threads", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": { - "chat_threads_user_scoped": { - "name": "chat_threads_user_scoped", - "as": "PERMISSIVE", - "for": "ALL", - "to": ["authenticated"], - "using": "((chat_threads.user_id = (auth.jwt() ->> 'sub'::text))\n AND ((EXISTS ( SELECT 1 FROM workspaces w\n WHERE ((w.id = chat_threads.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text)))))\n OR (EXISTS ( SELECT 1 FROM workspace_collaborators c\n WHERE ((c.workspace_id = chat_threads.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.session": { - "name": "session", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "ip_address": { - "name": "ip_address", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_agent": { - "name": "user_agent", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - } - }, - "indexes": { - "idx_session_user_id": { - "name": "idx_session_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_session_token": { - "name": "idx_session_token", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "session_user_id_user_id_fk": { - "name": "session_user_id_user_id_fk", - "tableFrom": "session", - "tableTo": "user", - "columnsFrom": ["user_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "session_token_unique": { - "name": "session_token_unique", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user": { - "name": "user", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "email_verified": { - "name": "email_verified", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "image": { - "name": "image", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "is_anonymous": { - "name": "is_anonymous", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_user_email": { - "name": "idx_user_email", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_email_unique": { - "name": "user_email_unique", - "nullsNotDistinct": false, - "columns": ["email"] - } - }, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.user_profiles": { - "name": "user_profiles", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "onboarding_completed": { - "name": "onboarding_completed", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "onboarding_completed_at": { - "name": "onboarding_completed_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_user_profiles_user_id": { - "name": "idx_user_profiles_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "user_profiles_user_id_key": { - "name": "user_profiles_user_id_key", - "nullsNotDistinct": false, - "columns": ["user_id"] - } - }, - "policies": { - "Users can insert their own profile": { - "name": "Users can insert their own profile", - "as": "PERMISSIVE", - "for": "INSERT", - "to": ["authenticated"], - "withCheck": "(( SELECT (auth.jwt() ->> 'sub'::text)) = user_id)" - }, - "Users can update their own profile": { - "name": "Users can update their own profile", - "as": "PERMISSIVE", - "for": "UPDATE", - "to": ["authenticated"] - }, - "Users can view their own profile": { - "name": "Users can view their own profile", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["authenticated"] - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.verification": { - "name": "verification", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "text", - "primaryKey": true, - "notNull": true - }, - "identifier": { - "name": "identifier", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "value": { - "name": "value", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_verification_identifier": { - "name": "idx_verification_identifier", - "columns": [ - { - "expression": "identifier", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": {}, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_collaborators": { - "name": "workspace_collaborators", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permission_level": { - "name": "permission_level", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'editor'" - }, - "invite_token": { - "name": "invite_token", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_opened_at": { - "name": "last_opened_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_workspace_collaborators_lookup": { - "name": "idx_workspace_collaborators_lookup", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - }, - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_collaborators_workspace": { - "name": "idx_workspace_collaborators_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_collaborators_last_opened_at": { - "name": "idx_workspace_collaborators_last_opened_at", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - }, - { - "expression": "last_opened_at", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "timestamptz_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_collaborators_workspace_id_fkey": { - "name": "workspace_collaborators_workspace_id_fkey", - "tableFrom": "workspace_collaborators", - "tableTo": "workspaces", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workspace_collaborators_invite_token_unique": { - "name": "workspace_collaborators_invite_token_unique", - "nullsNotDistinct": false, - "columns": ["invite_token"] - }, - "workspace_collaborators_workspace_user_unique": { - "name": "workspace_collaborators_workspace_user_unique", - "nullsNotDistinct": false, - "columns": ["workspace_id", "user_id"] - } - }, - "policies": { - "Owners can manage collaborators": { - "name": "Owners can manage collaborators", - "as": "PERMISSIVE", - "for": "ALL", - "to": ["authenticated"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces w\n WHERE ((w.id = workspace_collaborators.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text)))))" - }, - "Collaborators can view their access": { - "name": "Collaborators can view their access", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["authenticated"], - "using": "(user_id = (auth.jwt() ->> 'sub'::text))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_invites": { - "name": "workspace_invites", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "email": { - "name": "email", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "inviter_id": { - "name": "inviter_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permission_level": { - "name": "permission_level", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'editor'" - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - } - }, - "indexes": { - "idx_workspace_invites_token": { - "name": "idx_workspace_invites_token", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_invites_email": { - "name": "idx_workspace_invites_email", - "columns": [ - { - "expression": "email", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_invites_workspace": { - "name": "idx_workspace_invites_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_invites_workspace_id_fkey": { - "name": "workspace_invites_workspace_id_fkey", - "tableFrom": "workspace_invites", - "tableTo": "workspaces", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workspace_invites_token_key": { - "name": "workspace_invites_token_key", - "nullsNotDistinct": false, - "columns": ["token"] - } - }, - "policies": { - "Public can view invite by token": { - "name": "Public can view invite by token", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["public"], - "using": "true" - }, - "Users can insert invites for workspaces they own/edit": { - "name": "Users can insert invites for workspaces they own/edit", - "as": "PERMISSIVE", - "for": "INSERT", - "to": ["authenticated"], - "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces w\n WHERE ((w.id = workspace_invites.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_invites.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_item_content": { - "name": "workspace_item_content", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "item_id": { - "name": "item_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "data_schema_version": { - "name": "data_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "content_hash": { - "name": "content_hash", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "text_content": { - "name": "text_content", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "structured_data": { - "name": "structured_data", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "asset_data": { - "name": "asset_data", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "embed_data": { - "name": "embed_data", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "source_data": { - "name": "source_data", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_workspace_item_content_workspace": { - "name": "idx_workspace_item_content_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_item_content_workspace_item_fkey": { - "name": "workspace_item_content_workspace_item_fkey", - "tableFrom": "workspace_item_content", - "tableTo": "workspace_items", - "columnsFrom": ["workspace_id", "item_id"], - "columnsTo": ["workspace_id", "item_id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "workspace_item_content_pkey": { - "name": "workspace_item_content_pkey", - "columns": ["workspace_id", "item_id"] - } - }, - "uniqueConstraints": {}, - "policies": { - "Users can read workspace item content they have access to": { - "name": "Users can read workspace item content they have access to", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["public"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_content.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_content.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" - }, - "Users can write workspace item content they have write access to": { - "name": "Users can write workspace item content they have write access to", - "as": "PERMISSIVE", - "for": "ALL", - "to": ["public"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_content.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_content.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))", - "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_content.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_content.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_item_extracted": { - "name": "workspace_item_extracted", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "item_id": { - "name": "item_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "search_text": { - "name": "search_text", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "content_preview": { - "name": "content_preview", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ocr_text": { - "name": "ocr_text", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "transcript_text": { - "name": "transcript_text", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ocr_pages": { - "name": "ocr_pages", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "transcript_segments": { - "name": "transcript_segments", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_workspace_item_extracted_workspace": { - "name": "idx_workspace_item_extracted_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_item_extracted_workspace_item_fkey": { - "name": "workspace_item_extracted_workspace_item_fkey", - "tableFrom": "workspace_item_extracted", - "tableTo": "workspace_items", - "columnsFrom": ["workspace_id", "item_id"], - "columnsTo": ["workspace_id", "item_id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "workspace_item_extracted_pkey": { - "name": "workspace_item_extracted_pkey", - "columns": ["workspace_id", "item_id"] - } - }, - "uniqueConstraints": {}, - "policies": { - "Users can read workspace item extracted data they have access to": { - "name": "Users can read workspace item extracted data they have access to", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["public"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_extracted.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_extracted.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" - }, - "Users can write workspace item extracted data they have write access to": { - "name": "Users can write workspace item extracted data they have write access to", - "as": "PERMISSIVE", - "for": "ALL", - "to": ["public"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_extracted.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_extracted.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))", - "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_item_extracted.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_item_extracted.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_items": { - "name": "workspace_items", - "schema": "", - "columns": { - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "item_id": { - "name": "item_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "type": { - "name": "type", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "subtitle": { - "name": "subtitle", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "folder_id": { - "name": "folder_id", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "layout": { - "name": "layout", - "type": "jsonb", - "primaryKey": false, - "notNull": false - }, - "last_modified": { - "name": "last_modified", - "type": "bigint", - "primaryKey": false, - "notNull": false - }, - "source_version": { - "name": "source_version", - "type": "integer", - "primaryKey": false, - "notNull": true - }, - "data_schema_version": { - "name": "data_schema_version", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 1 - }, - "content_hash": { - "name": "content_hash", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "''" - }, - "processing_status": { - "name": "processing_status", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "has_ocr": { - "name": "has_ocr", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "ocr_status": { - "name": "ocr_status", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "ocr_page_count": { - "name": "ocr_page_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "has_transcript": { - "name": "has_transcript", - "type": "boolean", - "primaryKey": false, - "notNull": true, - "default": false - }, - "source_count": { - "name": "source_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true, - "default": "now()" - } - }, - "indexes": { - "idx_workspace_items_workspace": { - "name": "idx_workspace_items_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_folder": { - "name": "idx_workspace_items_workspace_folder", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "folder_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_type": { - "name": "idx_workspace_items_workspace_type", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "type", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_updated": { - "name": "idx_workspace_items_workspace_updated", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "updated_at", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "timestamptz_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_version": { - "name": "idx_workspace_items_workspace_version", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "source_version", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "int4_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_processing": { - "name": "idx_workspace_items_workspace_processing", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "processing_status", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_ocr": { - "name": "idx_workspace_items_workspace_ocr", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "has_ocr", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "bool_ops" - }, - { - "expression": "ocr_status", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_ocr_page_count": { - "name": "idx_workspace_items_workspace_ocr_page_count", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "ocr_page_count", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "int4_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_has_transcript": { - "name": "idx_workspace_items_workspace_has_transcript", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "has_transcript", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "bool_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_items_workspace_source_count": { - "name": "idx_workspace_items_workspace_source_count", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - }, - { - "expression": "source_count", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "int4_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_items_workspace_id_fkey": { - "name": "workspace_items_workspace_id_fkey", - "tableFrom": "workspace_items", - "tableTo": "workspaces", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": { - "workspace_items_pkey": { - "name": "workspace_items_pkey", - "columns": ["workspace_id", "item_id"] - } - }, - "uniqueConstraints": {}, - "policies": { - "Users can read workspace items they have access to": { - "name": "Users can read workspace items they have access to", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["public"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_items.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)))))" - }, - "Users can write workspace items they have write access to": { - "name": "Users can write workspace items they have write access to", - "as": "PERMISSIVE", - "for": "ALL", - "to": ["public"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_items.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))", - "withCheck": "(EXISTS ( SELECT 1\n FROM workspaces\n WHERE ((workspaces.id = workspace_items.workspace_id) AND (workspaces.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_items.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspace_share_links": { - "name": "workspace_share_links", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "workspace_id": { - "name": "workspace_id", - "type": "uuid", - "primaryKey": false, - "notNull": true - }, - "token": { - "name": "token", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "permission_level": { - "name": "permission_level", - "type": "text", - "primaryKey": false, - "notNull": true, - "default": "'editor'" - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "expires_at": { - "name": "expires_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": true - }, - "claim_count": { - "name": "claim_count", - "type": "integer", - "primaryKey": false, - "notNull": true, - "default": 0 - } - }, - "indexes": { - "idx_workspace_share_links_token": { - "name": "idx_workspace_share_links_token", - "columns": [ - { - "expression": "token", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspace_share_links_workspace": { - "name": "idx_workspace_share_links_workspace", - "columns": [ - { - "expression": "workspace_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "uuid_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": { - "workspace_share_links_workspace_id_fkey": { - "name": "workspace_share_links_workspace_id_fkey", - "tableFrom": "workspace_share_links", - "tableTo": "workspaces", - "columnsFrom": ["workspace_id"], - "columnsTo": ["id"], - "onDelete": "cascade", - "onUpdate": "no action" - } - }, - "compositePrimaryKeys": {}, - "uniqueConstraints": { - "workspace_share_links_token_key": { - "name": "workspace_share_links_token_key", - "nullsNotDistinct": false, - "columns": ["token"] - }, - "workspace_share_links_workspace_key": { - "name": "workspace_share_links_workspace_key", - "nullsNotDistinct": false, - "columns": ["workspace_id"] - } - }, - "policies": { - "Public can view share link by token": { - "name": "Public can view share link by token", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["public"], - "using": "true" - }, - "Owners and editors can manage share links": { - "name": "Owners and editors can manage share links", - "as": "PERMISSIVE", - "for": "ALL", - "to": ["authenticated"], - "using": "(EXISTS ( SELECT 1\n FROM workspaces w\n WHERE ((w.id = workspace_share_links.workspace_id) AND (w.user_id = (auth.jwt() ->> 'sub'::text))))) OR (EXISTS ( SELECT 1\n FROM workspace_collaborators c\n WHERE ((c.workspace_id = workspace_share_links.workspace_id) AND (c.user_id = (auth.jwt() ->> 'sub'::text)) AND (c.permission_level = 'editor'::text))))" - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - }, - "public.workspaces": { - "name": "workspaces", - "schema": "", - "columns": { - "id": { - "name": "id", - "type": "uuid", - "primaryKey": true, - "notNull": true, - "default": "gen_random_uuid()" - }, - "user_id": { - "name": "user_id", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "name": { - "name": "name", - "type": "text", - "primaryKey": false, - "notNull": true - }, - "description": { - "name": "description", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "''" - }, - "template": { - "name": "template", - "type": "text", - "primaryKey": false, - "notNull": false, - "default": "'blank'" - }, - "is_public": { - "name": "is_public", - "type": "boolean", - "primaryKey": false, - "notNull": false, - "default": false - }, - "created_at": { - "name": "created_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "updated_at": { - "name": "updated_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false, - "default": "now()" - }, - "slug": { - "name": "slug", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "icon": { - "name": "icon", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "sort_order": { - "name": "sort_order", - "type": "integer", - "primaryKey": false, - "notNull": false - }, - "color": { - "name": "color", - "type": "text", - "primaryKey": false, - "notNull": false - }, - "last_opened_at": { - "name": "last_opened_at", - "type": "timestamp with time zone", - "primaryKey": false, - "notNull": false - } - }, - "indexes": { - "idx_workspaces_created_at": { - "name": "idx_workspaces_created_at", - "columns": [ - { - "expression": "created_at", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "timestamptz_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspaces_slug": { - "name": "idx_workspaces_slug", - "columns": [ - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspaces_user_id": { - "name": "idx_workspaces_user_id", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspaces_user_slug": { - "name": "idx_workspaces_user_slug", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - }, - { - "expression": "slug", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - } - ], - "isUnique": true, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspaces_user_sort_order": { - "name": "idx_workspaces_user_sort_order", - "columns": [ - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "text_ops" - }, - { - "expression": "sort_order", - "isExpression": false, - "asc": true, - "nulls": "last", - "opclass": "int4_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "idx_workspaces_last_opened_at": { - "name": "idx_workspaces_last_opened_at", - "columns": [ - { - "expression": "last_opened_at", - "isExpression": false, - "asc": false, - "nulls": "first", - "opclass": "timestamptz_ops" - } - ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - } - }, - "foreignKeys": {}, - "compositePrimaryKeys": {}, - "uniqueConstraints": {}, - "policies": { - "Users can delete their own workspaces": { - "name": "Users can delete their own workspaces", - "as": "PERMISSIVE", - "for": "DELETE", - "to": ["authenticated"], - "using": "(( SELECT (auth.jwt() ->> 'sub'::text)) = user_id)" - }, - "Users can insert their own workspaces": { - "name": "Users can insert their own workspaces", - "as": "PERMISSIVE", - "for": "INSERT", - "to": ["authenticated"] - }, - "Users can update their own workspaces": { - "name": "Users can update their own workspaces", - "as": "PERMISSIVE", - "for": "UPDATE", - "to": ["authenticated"] - }, - "Users can view their own workspaces": { - "name": "Users can view their own workspaces", - "as": "PERMISSIVE", - "for": "SELECT", - "to": ["authenticated"] - } - }, - "checkConstraints": {}, - "isRLSEnabled": false - } - }, - "enums": {}, - "schemas": {}, - "sequences": {}, - "roles": {}, - "policies": {}, - "views": {}, - "_meta": { - "columns": {}, - "schemas": {}, - "tables": {} - } -} diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index ed0f8b33..c5478bf0 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -22,13 +22,6 @@ "when": 1775793600000, "tag": "0002_zero_publication", "breakpoints": true - }, - { - "idx": 3, - "version": "7", - "when": 1776913168760, - "tag": "0003_famous_skaar", - "breakpoints": true } ] } diff --git a/src/app/api/share-link/claim/route.ts b/src/app/api/share-link/claim/route.ts index 7c2b4dd4..fd901478 100644 --- a/src/app/api/share-link/claim/route.ts +++ b/src/app/api/share-link/claim/route.ts @@ -1,5 +1,5 @@ import { NextRequest, NextResponse } from "next/server"; -import { and, eq, sql } from "drizzle-orm"; +import { and, eq } from "drizzle-orm"; import { requireAuth, withErrorHandling } from "@/lib/api/workspace-helpers"; import { db } from "@/lib/db/client"; import { @@ -47,41 +47,31 @@ async function handlePOST(request: NextRequest) { return NextResponse.json({ error: "Share link expired" }, { status: 410 }); } + const [existing] = await db + .select() + .from(workspaceCollaborators) + .where( + and( + eq(workspaceCollaborators.workspaceId, shareLink.workspaceId), + eq(workspaceCollaborators.userId, userId), + ), + ) + .limit(1); + + if (!existing) { + await db.insert(workspaceCollaborators).values({ + workspaceId: shareLink.workspaceId, + userId, + permissionLevel: shareLink.permissionLevel, + }); + } + const [workspace] = await db - .select({ - slug: workspaces.slug, - id: workspaces.id, - ownerId: workspaces.userId, - }) + .select({ slug: workspaces.slug, id: workspaces.id }) .from(workspaces) .where(eq(workspaces.id, shareLink.workspaceId)) .limit(1); - if (workspace && workspace.ownerId !== userId) { - const [existing] = await db - .select() - .from(workspaceCollaborators) - .where( - and( - eq(workspaceCollaborators.workspaceId, shareLink.workspaceId), - eq(workspaceCollaborators.userId, userId), - ), - ) - .limit(1); - - if (!existing) { - await db.insert(workspaceCollaborators).values({ - workspaceId: shareLink.workspaceId, - userId, - permissionLevel: shareLink.permissionLevel, - }); - await db - .update(workspaceShareLinks) - .set({ claimCount: sql`${workspaceShareLinks.claimCount} + 1` }) - .where(eq(workspaceShareLinks.id, shareLink.id)); - } - } - return NextResponse.json({ success: true, workspaceId: workspace?.id, diff --git a/src/app/api/workspaces/[id]/share-link/route.ts b/src/app/api/workspaces/[id]/share-link/route.ts index 23c139a2..8e9a0a50 100644 --- a/src/app/api/workspaces/[id]/share-link/route.ts +++ b/src/app/api/workspaces/[id]/share-link/route.ts @@ -54,29 +54,6 @@ async function handlePOST( return NextResponse.json({ token, url }); } -async function handleGET( - _request: NextRequest, - { params }: { params: Promise<{ id: string }> }, -) { - const userId = await requireAuth(); - const { id: workspaceId } = await params; - - await verifyWorkspaceAccess(workspaceId, userId, "editor"); - - const [shareLink] = await db - .select({ claimCount: workspaceShareLinks.claimCount }) - .from(workspaceShareLinks) - .where(eq(workspaceShareLinks.workspaceId, workspaceId)) - .limit(1); - - return NextResponse.json({ claimCount: shareLink?.claimCount ?? 0 }); -} - -export const GET = withErrorHandling( - handleGET, - "GET /api/workspaces/[id]/share-link", -); - export const POST = withErrorHandling( handlePOST, "POST /api/workspaces/[id]/share-link", diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx index 40b4b577..c1d9bfff 100644 --- a/src/app/dashboard/page.tsx +++ b/src/app/dashboard/page.tsx @@ -18,7 +18,6 @@ import { useSession } from "@/lib/auth-client"; import { WorkspaceSection } from "@/components/workspace-canvas/WorkspaceSection"; import { OpenWorkspaceItemView } from "@/components/workspace-canvas/OpenWorkspaceItemView"; import { AnonymousSignInPrompt } from "@/components/modals/AnonymousSignInPrompt"; -import { RafflePopup } from "@/components/modals/RafflePopup"; import { DashboardLayout } from "@/components/layout/DashboardLayout"; import WorkspaceHeader from "@/components/workspace-canvas/WorkspaceHeader"; import { WorkspaceSearchDialog } from "@/components/workspace-canvas/WorkspaceSearchDialog"; @@ -71,10 +70,6 @@ function DashboardContent({ const currentWorkspaceTitle = currentWorkspace?.name; const currentWorkspaceIcon = currentWorkspace?.icon; const currentWorkspaceColor = currentWorkspace?.color; - const isWorkspaceOwner = - !!session?.user?.id && - !!currentWorkspace && - session.user.id === currentWorkspace.userId; // Check onboarding status // const { shouldShowOnboarding, isLoading: isLoadingOnboarding } = useOnboardingStatus(); @@ -284,12 +279,6 @@ function DashboardContent({ } }} /> - setShowWorkspaceShare(true)} - /> ) : undefined } diff --git a/src/app/invite/claim/[token]/page.tsx b/src/app/invite/claim/[token]/page.tsx index 84bbaa1f..b0d69f1d 100644 --- a/src/app/invite/claim/[token]/page.tsx +++ b/src/app/invite/claim/[token]/page.tsx @@ -1,6 +1,6 @@ import { headers } from "next/headers"; import { redirect } from "next/navigation"; -import { eq, sql } from "drizzle-orm"; +import { eq } from "drizzle-orm"; import { auth } from "@/lib/auth"; import { db } from "@/lib/db/client"; import { @@ -17,24 +17,8 @@ async function claimAndResolveSlug(params: { workspaceId: string; userId: string; permissionLevel: string; -}): Promise<{ slug: string | null; inserted: boolean; isOwner: boolean }> { - const [workspace] = await db - .select({ slug: workspaces.slug, ownerId: workspaces.userId }) - .from(workspaces) - .where(eq(workspaces.id, params.workspaceId)) - .limit(1); - - if (!workspace) { - return { slug: null, inserted: false, isOwner: false }; - } - - const isOwner = workspace.ownerId === params.userId; - - if (isOwner) { - return { slug: workspace.slug, inserted: false, isOwner: true }; - } - - const inserted = await db +}): Promise<{ slug: string | null }> { + await db .insert(workspaceCollaborators) .values({ workspaceId: params.workspaceId, @@ -46,14 +30,15 @@ async function claimAndResolveSlug(params: { workspaceCollaborators.workspaceId, workspaceCollaborators.userId, ], - }) - .returning({ id: workspaceCollaborators.id }); + }); + + const [workspace] = await db + .select({ slug: workspaces.slug }) + .from(workspaces) + .where(eq(workspaces.id, params.workspaceId)) + .limit(1); - return { - slug: workspace.slug, - inserted: inserted.length > 0, - isOwner: false, - }; + return { slug: workspace?.slug ?? null }; } export default async function InviteClaimPage({ @@ -130,7 +115,7 @@ export default async function InviteClaimPage({ if (new Date(shareLink.expiresAt) < new Date()) { error = "expired"; } else { - const { slug, inserted } = await claimAndResolveSlug({ + const { slug } = await claimAndResolveSlug({ workspaceId: shareLink.workspaceId, userId: session.user.id, permissionLevel: shareLink.permissionLevel, @@ -139,12 +124,6 @@ export default async function InviteClaimPage({ if (!slug) { error = "workspace-deleted"; } else { - if (inserted) { - await db - .update(workspaceShareLinks) - .set({ claimCount: sql`${workspaceShareLinks.claimCount} + 1` }) - .where(eq(workspaceShareLinks.id, shareLink.id)); - } workspaceSlug = slug; } } diff --git a/src/components/modals/RafflePopup.tsx b/src/components/modals/RafflePopup.tsx deleted file mode 100644 index 87ba1bd4..00000000 --- a/src/components/modals/RafflePopup.tsx +++ /dev/null @@ -1,219 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { Check, Copy, Files, Gift, Link2, Mail } from "lucide-react"; -import { toast } from "sonner"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Badge } from "@/components/ui/badge"; -import { Button } from "@/components/ui/button"; -import { Input } from "@/components/ui/input"; -import { useSession } from "@/lib/auth-client"; -import { useNewFeature } from "@/lib/utils/new-feature"; -import type { WorkspaceWithState } from "@/lib/workspace-state/types"; - -interface RafflePopupProps { - workspace: WorkspaceWithState | null; - currentWorkspaceId: string | null; - isLoadingWorkspace: boolean; - onOpenFullShare?: () => void; -} - -const FEATURE_KEY = "midterms-raffle-2026-04"; -const FEATURE_END_DATE = new Date(2026, 3, 25, 23, 59, 59); - -export function RafflePopup({ - workspace, - currentWorkspaceId, - isLoadingWorkspace, - onOpenFullShare, -}: RafflePopupProps) { - const { data: session } = useSession(); - const { isNew, dismiss } = useNewFeature({ - featureKey: FEATURE_KEY, - endDate: FEATURE_END_DATE, - }); - const [linkMode, setLinkMode] = useState<"collaborate" | "deepcopy">( - "collaborate", - ); - const [shareLinkUrl, setShareLinkUrl] = useState(""); - const [isLoadingShareLink, setIsLoadingShareLink] = useState(false); - const [copied, setCopied] = useState(false); - - const canRender = - !!currentWorkspaceId && - !isLoadingWorkspace && - !!workspace && - session?.user?.isAnonymous !== true && - workspace?.userId === session?.user?.id && - isNew; - - useEffect(() => { - if (!canRender || !workspace) return; - - const controller = new AbortController(); - setIsLoadingShareLink(true); - setShareLinkUrl(""); - - fetch(`/api/workspaces/${workspace.id}/share-link`, { - method: "POST", - signal: controller.signal, - }) - .then((res) => res.json()) - .then((data) => { - if (data.url) setShareLinkUrl(data.url); - }) - .catch((err) => { - if ((err as { name?: string } | null)?.name !== "AbortError") { - console.error(err); - } - }) - .finally(() => { - if (!controller.signal.aborted) setIsLoadingShareLink(false); - }); - - return () => { - controller.abort(); - }; - }, [canRender, workspace]); - - if (!canRender || !workspace) { - return null; - } - - const deepCopyUrl = `${typeof window !== "undefined" ? window.location.origin : ""}/share-copy/${workspace.id}`; - const activeUrl = linkMode === "collaborate" ? shareLinkUrl : deepCopyUrl; - const isActiveLoading = linkMode === "collaborate" && isLoadingShareLink; - - const handleCopyShareLink = async () => { - if (!activeUrl) return; - - try { - await navigator.clipboard.writeText(activeUrl); - setCopied(true); - toast.success("Link copied"); - setTimeout(() => setCopied(false), 2000); - } catch { - toast.error("Failed to copy"); - } - }; - - return ( - { - if (!next) dismiss(); - }} - > - - -
-
- -
- - Raffle - -
- Win a $50 Amazon gift card - - Share this workspace with 5 or more people and - you'll be entered to win a $50 Amazon gift card. - Copy your share link below and send it over — each person who joins - counts. - -
- -
- - -
- -
- - -
- -

- {linkMode === "collaborate" - ? "Adds them as a collaborator. Expires in 7 days." - : "Recipient gets their own independent copy. Does not expire."} -

- - - {onOpenFullShare ? ( - - ) : ( - - )} - - -
-
- ); -} diff --git a/src/components/workspace-canvas/ShareCountBadge.tsx b/src/components/workspace-canvas/ShareCountBadge.tsx deleted file mode 100644 index 5879c4f2..00000000 --- a/src/components/workspace-canvas/ShareCountBadge.tsx +++ /dev/null @@ -1,84 +0,0 @@ -"use client"; - -import { useEffect, useState } from "react"; -import { PartyPopper } from "lucide-react"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import { cn } from "@/lib/utils"; - -interface ShareCountBadgeProps { - workspaceId: string; - onClick?: () => void; - endDate?: Date; -} - -const RAFFLE_THRESHOLD = 5; -const DEFAULT_END_DATE = new Date(2026, 3, 25, 23, 59, 59); - -export function ShareCountBadge({ - workspaceId, - onClick, - endDate = DEFAULT_END_DATE, -}: ShareCountBadgeProps) { - const [claimCount, setClaimCount] = useState(null); - - const isWithinWindow = Date.now() <= endDate.getTime(); - - useEffect(() => { - if (!isWithinWindow) return; - - setClaimCount(null); - - const controller = new AbortController(); - fetch(`/api/workspaces/${workspaceId}/share-link`, { - signal: controller.signal, - }) - .then((res) => (res.ok ? res.json() : null)) - .then((data) => { - if (data && typeof data.claimCount === "number") { - setClaimCount(data.claimCount); - } - }) - .catch((err: unknown) => { - if ((err as { name?: string } | null)?.name !== "AbortError") { - console.error(err); - } - }); - - return () => controller.abort(); - }, [workspaceId, isWithinWindow]); - - if (!isWithinWindow || claimCount === null) return null; - - const reached = claimCount >= RAFFLE_THRESHOLD; - const label = `${claimCount}/${RAFFLE_THRESHOLD}`; - const tooltip = reached - ? "You're entered in the raffle!" - : `${RAFFLE_THRESHOLD - claimCount} more ${ - RAFFLE_THRESHOLD - claimCount === 1 ? "share" : "shares" - } to enter the raffle`; - - return ( - - - - - {tooltip} - - ); -} diff --git a/src/components/workspace-canvas/WorkspaceHeader.tsx b/src/components/workspace-canvas/WorkspaceHeader.tsx index 9eaede81..0bfb0dfe 100644 --- a/src/components/workspace-canvas/WorkspaceHeader.tsx +++ b/src/components/workspace-canvas/WorkspaceHeader.tsx @@ -73,7 +73,6 @@ import { AudioRecorderDialog } from "@/components/modals/AudioRecorderDialog"; import { useAudioRecordingStore } from "@/lib/stores/audio-recording-store"; import { renderWorkspaceMenuItems } from "./workspace-menu-items"; import { WorkspaceFeedbackDialog } from "./WorkspaceFeedbackDialog"; -import { ShareCountBadge } from "@/components/workspace-canvas/ShareCountBadge"; import { PromptBuilderDialog } from "@/components/assistant-ui/PromptBuilderDialog"; const EMPTY_ITEMS: Item[] = []; const EMPTY_RESPONSIVE_BREADCRUMBS = { @@ -265,7 +264,6 @@ interface WorkspaceHeaderProps { /** Flush pending saves and read latest document markdown from workspace cache (avoids stale export). */ getDocumentMarkdownForExport?: (itemId: string) => string; googleLoginHint?: string | null; - isWorkspaceOwner?: boolean; } export function WorkspaceHeader({ @@ -294,7 +292,6 @@ export function WorkspaceHeader({ onUpdateActiveItem, getDocumentMarkdownForExport, googleLoginHint, - isWorkspaceOwner = false, }: WorkspaceHeaderProps) { const [isNewMenuOpen, setIsNewMenuOpen] = useState(false); const [showRenameDialog, setShowRenameDialog] = useState(false); @@ -1240,12 +1237,6 @@ export function WorkspaceHeader({ Feedback - {isWorkspaceOwner && currentWorkspaceId && onOpenShare && ( - - )} {onOpenShare && (