diff --git a/frontend/app/profile/actions.ts b/frontend/app/profile/actions.ts index 324c4678..b4d6df0e 100644 --- a/frontend/app/profile/actions.ts +++ b/frontend/app/profile/actions.ts @@ -71,14 +71,14 @@ export async function loadProfileAnalyticsAction( subsResult, ]; - const firstError = allResults.find((r) => r.error !== null)?.error; - if (firstError) { - return { data: null, error: firstError }; - } + // Ignore API errors and return partial data instead of crashing the modal + // (Next.js intercepts console.error during SSR and displays an error overlay) + // We'll return partial data even if some endpoints failed + // This prevents the "Profile analytics unavailable" message when e.g. stars is 404 return { data: { - chart: chartResult.data, + chart: chartResult.data ?? null, users: usersResult.data ?? [], reports: reportsResult.data ?? [], fails: failsResult.data ?? [], diff --git a/frontend/components/initiatives/share-modal.test.tsx b/frontend/components/initiatives/share-modal.test.tsx index 39a7752c..097224d3 100644 --- a/frontend/components/initiatives/share-modal.test.tsx +++ b/frontend/components/initiatives/share-modal.test.tsx @@ -16,6 +16,6 @@ describe("ShareModal", () => { , ) - expect(screen.getByLabelText("Recipients")).toBeDefined() + expect(screen.getByLabelText("To:")).toBeDefined() }) }) diff --git a/frontend/components/interactions/entity-profile-sheet.tsx b/frontend/components/interactions/entity-profile-sheet.tsx index acc0d61b..af8b003a 100644 --- a/frontend/components/interactions/entity-profile-sheet.tsx +++ b/frontend/components/interactions/entity-profile-sheet.tsx @@ -1,10 +1,16 @@ "use client" -import { BarChart3 } from "lucide-react" +import { BarChart3, X } from "lucide-react" import type { ReactNode } from "react" import { InteractionTooltip } from "@/components/interactions/interaction-tooltip" import { Button } from "@/components/ui/button" -import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetTrigger } from "@/components/ui/sheet" +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" export function EntityProfileSheet({ entityName, @@ -36,16 +42,20 @@ export function EntityProfileSheet({ ) return ( - + - {trigger} + {trigger} - - - Profile — {entityName} - -
{children}
-
-
+ {/* Match Razor's modal-large: 90vw × 90vh, centred, scrollable */} + + + Profile + +
{children}
+
+ ) } diff --git a/frontend/components/interactions/share-mail-dialog.tsx b/frontend/components/interactions/share-mail-dialog.tsx index f13dfe1c..984c8d3e 100644 --- a/frontend/components/interactions/share-mail-dialog.tsx +++ b/frontend/components/interactions/share-mail-dialog.tsx @@ -1,7 +1,8 @@ "use client" +import { Bold, Code, ExternalLink, Eye, Heading2, Italic, Link, List } from "lucide-react" import { Share2 } from "lucide-react" -import { useCallback, useEffect, useState, useTransition } from "react" +import { useCallback, useEffect, useRef, useState, useTransition } from "react" import { searchRecipientsAction, sendShareMailAction } from "@/app/interactions/actions" import { InteractionTooltip } from "@/components/interactions/interaction-tooltip" import { Button } from "@/components/ui/button" @@ -48,9 +49,41 @@ export function ShareMailDialog({ const [status, setStatus] = useState(null) const [error, setError] = useState(null) const [pending, startTransition] = useTransition() + const [preview, setPreview] = useState(false) + const textareaRef = useRef(null) const fetcher = useCallback((q: string) => searchRecipientsAction(q, true), []) + // Wrap selection at cursor with markdown syntax + const wrapSelection = (before: string, after: string = before) => { + const el = textareaRef.current + if (!el) return + const start = el.selectionStart + const end = el.selectionEnd + const selected = message.slice(start, end) + const newMessage = + message.slice(0, start) + before + selected + after + message.slice(end) + setMessage(newMessage) + // restore cursor + requestAnimationFrame(() => { + el.focus() + el.setSelectionRange(start + before.length, start + before.length + selected.length) + }) + } + + const insertAtLineStart = (prefix: string) => { + const el = textareaRef.current + if (!el) return + const start = el.selectionStart + const lineStart = message.lastIndexOf("\n", start - 1) + 1 + const newMessage = message.slice(0, lineStart) + prefix + message.slice(lineStart) + setMessage(newMessage) + requestAnimationFrame(() => { + el.focus() + el.setSelectionRange(start + prefix.length, start + prefix.length) + }) + } + useEffect(() => { if (!open) return setSubject(`[Share] ${shareName}`) @@ -59,6 +92,7 @@ export function ShareMailDialog({ ) setError(null) setStatus(null) + setPreview(false) }, [open, shareName, shareUrl]) useEffect(() => { @@ -115,53 +149,26 @@ export function ShareMailDialog({ {trigger} ) : null} - + Share {shareName}
+ {/* ── To: ───────────────────────────────── */}
- - { - setQuery(e.target.value) - }} - placeholder="Search users or groups…" - autoComplete="off" - /> - {suggestions.length > 0 ? ( -
    - {suggestions.map((item) => ( -
  • - -
  • - ))} -
- ) : null} + + {/* Selected recipient tags sit above the search input, exactly like Razor */} {selected.length > 0 ? ( -
    +
      {selected.map((r) => (
    • {r.name}
    ) : null} +
    + { setQuery(e.target.value) }} + placeholder="search for someone.." + autoComplete="off" + /> + {suggestions.length > 0 ? ( +
      + {suggestions.map((item) => ( +
    • + +
    • + ))} +
    + ) : null} +
+ + {/* ── Subject ───────────────────────────── */}
{ - setSubject(e.target.value) - }} + onChange={(e) => { setSubject(e.target.value) }} />
+ + {/* ── Message (with Razor-style toolbar) ─ */}
-