From 0d62a4ca7934444119e212f04093a3c50307c137 Mon Sep 17 00:00:00 2001 From: Alireza Hassani Date: Mon, 31 Aug 2026 18:09:09 +0330 Subject: [PATCH] Add parked-domain aliases, Websites AI prompts, and frosted overflow menus. Co-authored-by: Cursor --- .../BusinessActionsOverflowMenu.module.css | 25 +- .../src/components/DomainAiPromptsModal.tsx | 350 ++++++++++++++++++ .../components/DomainLinksOverflowMenu.tsx | 22 +- .../components/ParkedDomainsModal.module.css | 122 ++++++ .../src/components/ParkedDomainsModal.tsx | 201 ++++++++++ apps/super-admin/src/pages/BusinessesPage.tsx | 21 +- .../src/pages/WebsitesPage.module.css | 13 + apps/super-admin/src/pages/WebsitesPage.tsx | 117 +++++- .../super-admin/src/services/domainService.ts | 15 +- apps/super-admin/src/types/domain.ts | 28 ++ docs/PROJECT_CONTEXT.md | 4 +- 11 files changed, 892 insertions(+), 26 deletions(-) create mode 100644 apps/super-admin/src/components/DomainAiPromptsModal.tsx create mode 100644 apps/super-admin/src/components/ParkedDomainsModal.module.css create mode 100644 apps/super-admin/src/components/ParkedDomainsModal.tsx diff --git a/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css b/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css index 747e943..3ebaf81 100644 --- a/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css +++ b/apps/super-admin/src/components/BusinessActionsOverflowMenu.module.css @@ -29,16 +29,21 @@ z-index: 1200; min-width: 220px; padding: 6px; - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.95); - background: rgba(255, 255, 255, 0.9); - backdrop-filter: blur(80px) saturate(1.7); - -webkit-backdrop-filter: blur(80px) saturate(1.7); + border-radius: 14px; + border: 1px solid var(--glass-border); + background: + linear-gradient( + 180deg, + rgba(255, 255, 255, 0.72) 0%, + var(--glass-bg-strong) 100% + ); + backdrop-filter: blur(var(--blur-glass)) saturate(1.85); + -webkit-backdrop-filter: blur(var(--blur-glass)) saturate(1.85); box-shadow: - 0 12px 40px rgba(15, 23, 42, 0.18), - inset 0 1px 0 rgba(255, 255, 255, 0.8); - isolation: isolate; - animation: menuIn 0.14s ease; + 0 18px 48px rgba(15, 23, 42, 0.2), + 0 0 0 1px rgba(255, 255, 255, 0.45), + inset 0 1px 0 rgba(255, 255, 255, 0.95); + animation: menuIn 0.16s ease; } .item { @@ -97,10 +102,8 @@ @keyframes menuIn { from { opacity: 0; - transform: translateY(-4px); } to { opacity: 1; - transform: translateY(0); } } diff --git a/apps/super-admin/src/components/DomainAiPromptsModal.tsx b/apps/super-admin/src/components/DomainAiPromptsModal.tsx new file mode 100644 index 0000000..c5cc70f --- /dev/null +++ b/apps/super-admin/src/components/DomainAiPromptsModal.tsx @@ -0,0 +1,350 @@ +import { useEffect, useState } from 'react' +import { Copy, FileText, Pencil, Plus, Trash2 } from 'lucide-react' +import { Modal } from './Modal' +import { useToast } from '../context/ToastContext' +import { ApiError } from '../lib/api' +import { + createDomainAiPrompt, + deleteDomainAiPrompt, + listAiPromptTemplates, + listDomainAiPrompts, + updateDomainAiPrompt, +} from '../services/aiPromptService' +import type { AiPromptTemplate, DomainAiPrompt } from '../types/aiPrompt' +import styles from '../pages/BusinessesPage.module.css' + +interface DomainAiPromptsModalProps { + open: boolean + title: string + domainId: number | null + onClose: () => void +} + +export function DomainAiPromptsModal({ + open, + title, + domainId, + onClose, +}: DomainAiPromptsModalProps) { + const { showToast } = useToast() + const [domainPrompts, setDomainPrompts] = useState([]) + const [allPromptTemplates, setAllPromptTemplates] = useState([]) + const [promptsLoading, setPromptsLoading] = useState(false) + const [promptsError, setPromptsError] = useState('') + const [promptEditorOpen, setPromptEditorOpen] = useState(false) + const [editingDomainPrompt, setEditingDomainPrompt] = useState(null) + const [promptDraftTemplateId, setPromptDraftTemplateId] = useState('') + const [promptDraftName, setPromptDraftName] = useState('') + const [promptDraftBody, setPromptDraftBody] = useState('') + const [promptEditorSubmitting, setPromptEditorSubmitting] = useState(false) + const [promptEditorError, setPromptEditorError] = useState('') + const [promptBusyId, setPromptBusyId] = useState(null) + + useEffect(() => { + if (!open) return + + setPromptsError('') + setDomainPrompts([]) + setAllPromptTemplates([]) + setPromptEditorOpen(false) + setEditingDomainPrompt(null) + + if (domainId == null) { + setPromptsError('Add a website/domain before assigning AI prompts.') + return + } + + const controller = new AbortController() + setPromptsLoading(true) + + void (async () => { + try { + const [allTemplates, assigned] = await Promise.all([ + listAiPromptTemplates(controller.signal), + listDomainAiPrompts(domainId, controller.signal), + ]) + if (controller.signal.aborted) return + setAllPromptTemplates(allTemplates.items) + setDomainPrompts(assigned.items) + } catch (err) { + if (controller.signal.aborted) return + setPromptsError( + err instanceof ApiError ? err.message : 'Unable to load AI prompts.', + ) + } finally { + if (!controller.signal.aborted) setPromptsLoading(false) + } + })() + + return () => { + controller.abort() + } + }, [open, domainId]) + + function openAddDomainPrompt() { + setEditingDomainPrompt(null) + setPromptDraftTemplateId('') + setPromptDraftName('') + setPromptDraftBody('') + setPromptEditorError('') + setPromptEditorOpen(true) + } + + function openEditDomainPrompt(prompt: DomainAiPrompt) { + setEditingDomainPrompt(prompt) + setPromptDraftTemplateId(prompt.sourceTemplateId ?? '') + setPromptDraftName(prompt.name) + setPromptDraftBody(prompt.body) + setPromptEditorError('') + setPromptEditorOpen(true) + } + + function applyTemplateToDraft(templateId: string) { + setPromptDraftTemplateId(templateId) + const template = allPromptTemplates.find((item) => item.id === templateId) + if (!template) return + setPromptDraftName(template.name) + setPromptDraftBody(template.body) + } + + async function submitDomainPromptEditor() { + if (domainId == null) return + const name = promptDraftName.trim() + const body = promptDraftBody.trim() + if (!name) { + setPromptEditorError('Name is required.') + return + } + if (!body) { + setPromptEditorError('Prompt text is required.') + return + } + + setPromptEditorSubmitting(true) + setPromptEditorError('') + try { + if (editingDomainPrompt) { + const updated = await updateDomainAiPrompt(domainId, editingDomainPrompt.id, { + name, + body, + }) + setDomainPrompts((prev) => + prev.map((item) => (item.id === updated.id ? updated : item)), + ) + showToast('Prompt updated.', 'success') + } else { + const created = await createDomainAiPrompt(domainId, { + name, + body, + ...(promptDraftTemplateId + ? { sourceTemplateId: Number(promptDraftTemplateId) } + : {}), + }) + setDomainPrompts((prev) => [...prev, created]) + showToast('Prompt added.', 'success') + } + setPromptEditorOpen(false) + setEditingDomainPrompt(null) + } catch (err) { + setPromptEditorError( + err instanceof ApiError ? err.message : 'Unable to save prompt.', + ) + } finally { + setPromptEditorSubmitting(false) + } + } + + async function removeDomainPromptItem(prompt: DomainAiPrompt) { + if (domainId == null) return + setPromptBusyId(prompt.id) + try { + await deleteDomainAiPrompt(domainId, prompt.id) + setDomainPrompts((prev) => prev.filter((item) => item.id !== prompt.id)) + showToast('Prompt removed.', 'success') + } catch (err) { + showToast( + err instanceof ApiError ? err.message : 'Unable to remove prompt.', + 'error', + ) + } finally { + setPromptBusyId(null) + } + } + + async function copyDomainPromptLink(url: string) { + try { + await navigator.clipboard.writeText(url) + showToast('Public link copied.', 'success') + } catch { + showToast('Unable to copy link.', 'error') + } + } + + function closeList() { + if (promptEditorSubmitting) return + setPromptEditorOpen(false) + onClose() + } + + return ( + <> + + {promptsError ? ( +

+ {promptsError} +

+ ) : null} + + {promptsLoading ? ( +

Loading…

+ ) : domainId != null ? ( + domainPrompts.length === 0 ? ( +
+
+ ) : ( +
    + {domainPrompts.map((prompt) => ( +
  • +
    + {prompt.name} + {prompt.body} +
    +
    + + + +
    +
  • + ))} +
+ ) + ) : null} + +
+
+ + +
+ + + { + if (promptEditorSubmitting) return + setPromptEditorOpen(false) + setEditingDomainPrompt(null) + setPromptEditorError('') + }} + > + {promptEditorError ? ( +

+ {promptEditorError} +

+ ) : null} +
+ + +
+
+ + setPromptDraftName(e.target.value)} + placeholder="Prompt name" + disabled={promptEditorSubmitting} + /> +
+
+ +