Add Super Admin AI prompt templates and business assignment UI.
Templates library plus per-business prompt list with edit, copy public link, and remove. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
b50e4fd812
commit
0aab7b5529
@@ -4,6 +4,7 @@ import {
|
||||
Building2,
|
||||
Eraser,
|
||||
ExternalLink,
|
||||
FileText,
|
||||
Globe,
|
||||
MoreHorizontal,
|
||||
Trash2,
|
||||
@@ -22,6 +23,7 @@ interface BusinessActionsOverflowMenuProps {
|
||||
businessName: string
|
||||
/** Primary apex domain host when set — enables website / dashboard links. */
|
||||
host?: string | null
|
||||
onAiPrompts: () => void
|
||||
onAllUsers: () => void
|
||||
onWipeData: () => void
|
||||
onRemove: () => void
|
||||
@@ -46,6 +48,7 @@ function openHost(host: string) {
|
||||
export function BusinessActionsOverflowMenu({
|
||||
businessName,
|
||||
host,
|
||||
onAiPrompts,
|
||||
onAllUsers,
|
||||
onWipeData,
|
||||
onRemove,
|
||||
@@ -194,6 +197,18 @@ export function BusinessActionsOverflowMenu({
|
||||
</button>
|
||||
))}
|
||||
{linkItems.length > 0 && <div className={styles.menuDivider} role="separator" />}
|
||||
<button
|
||||
type="button"
|
||||
className={styles.item}
|
||||
role="menuitem"
|
||||
onClick={() => {
|
||||
setOpen(false)
|
||||
onAiPrompts()
|
||||
}}
|
||||
>
|
||||
<FileText size={16} />
|
||||
<span>AI prompts</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={styles.item}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { NavLink, useNavigate } from 'react-router-dom'
|
||||
import { Home, Building2, Users, Globe, Settings, HelpCircle, LogOut } from 'lucide-react'
|
||||
import { Home, Building2, Users, Globe, FileText, Settings, HelpCircle, LogOut } from 'lucide-react'
|
||||
import { useAuth } from '../context/AuthContext'
|
||||
import meshkeeLogo from '../assets/meshkee-logo.png'
|
||||
import styles from './Sidebar.module.css'
|
||||
@@ -9,6 +9,7 @@ const navItems = [
|
||||
{ icon: Building2, label: 'Businesses', to: '/businesses' },
|
||||
{ icon: Users, label: 'Users', to: '/users' },
|
||||
{ icon: Globe, label: 'Websites', to: '/websites' },
|
||||
{ icon: FileText, label: 'Templates', to: '/templates' },
|
||||
{ icon: Settings, label: 'Settings', to: '/settings' },
|
||||
]
|
||||
|
||||
|
||||
@@ -56,10 +56,12 @@
|
||||
}
|
||||
|
||||
.panel {
|
||||
position: static;
|
||||
margin-top: 6px;
|
||||
position: fixed;
|
||||
z-index: 400;
|
||||
width: 300px;
|
||||
max-width: calc(100vw - 24px);
|
||||
padding: 12px;
|
||||
background: rgba(255, 255, 255, 0.95);
|
||||
background: rgba(255, 255, 255, 0.97);
|
||||
backdrop-filter: blur(16px);
|
||||
-webkit-backdrop-filter: blur(16px);
|
||||
border: 1px solid var(--glass-border);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useEffect, useId, useMemo, useRef, useState } from 'react'
|
||||
import { useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from 'react'
|
||||
import { createPortal } from 'react-dom'
|
||||
import { CalendarDays, ChevronLeft } from 'lucide-react'
|
||||
import styles from './StepDatePicker.module.css'
|
||||
|
||||
@@ -12,6 +13,8 @@ interface StepDatePickerProps {
|
||||
placeholder?: string
|
||||
}
|
||||
|
||||
const PANEL_WIDTH = 300
|
||||
|
||||
const MONTHS = [
|
||||
'Jan',
|
||||
'Feb',
|
||||
@@ -81,8 +84,12 @@ export function StepDatePicker({
|
||||
const autoId = useId()
|
||||
const fieldId = id ?? autoId
|
||||
const rootRef = useRef<HTMLDivElement>(null)
|
||||
const panelRef = useRef<HTMLDivElement>(null)
|
||||
const [open, setOpen] = useState(false)
|
||||
const [step, setStep] = useState<Step>('year')
|
||||
const [panelPos, setPanelPos] = useState<{ top: number; left: number } | null>(
|
||||
null,
|
||||
)
|
||||
|
||||
const today = useMemo(() => {
|
||||
const now = new Date()
|
||||
@@ -103,12 +110,41 @@ export function StepDatePicker({
|
||||
return Array.from({ length: end - start + 1 }, (_, i) => start + i)
|
||||
}, [today.year])
|
||||
|
||||
function updatePanelPosition() {
|
||||
const trigger = rootRef.current
|
||||
if (!trigger) return
|
||||
const rect = trigger.getBoundingClientRect()
|
||||
const width = Math.min(PANEL_WIDTH, window.innerWidth - 24)
|
||||
let left = rect.right - width
|
||||
if (left < 12) left = 12
|
||||
if (left + width > window.innerWidth - 12) {
|
||||
left = Math.max(12, window.innerWidth - 12 - width)
|
||||
}
|
||||
setPanelPos({ top: rect.bottom + 4, left })
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!open) {
|
||||
setPanelPos(null)
|
||||
return
|
||||
}
|
||||
updatePanelPosition()
|
||||
const onReposition = () => updatePanelPosition()
|
||||
window.addEventListener('resize', onReposition)
|
||||
window.addEventListener('scroll', onReposition, true)
|
||||
return () => {
|
||||
window.removeEventListener('resize', onReposition)
|
||||
window.removeEventListener('scroll', onReposition, true)
|
||||
}
|
||||
}, [open])
|
||||
|
||||
useEffect(() => {
|
||||
if (!open) return
|
||||
const onPointerDown = (event: MouseEvent) => {
|
||||
if (!rootRef.current?.contains(event.target as Node)) {
|
||||
setOpen(false)
|
||||
}
|
||||
const target = event.target as Node
|
||||
if (rootRef.current?.contains(target)) return
|
||||
if (panelRef.current?.contains(target)) return
|
||||
setOpen(false)
|
||||
}
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') setOpen(false)
|
||||
@@ -155,6 +191,139 @@ export function StepDatePicker({
|
||||
? today.day
|
||||
: null
|
||||
|
||||
const panel =
|
||||
open && panelPos
|
||||
? createPortal(
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={styles.panel}
|
||||
role="dialog"
|
||||
aria-label="Choose date"
|
||||
style={{ top: panelPos.top, left: panelPos.left }}
|
||||
>
|
||||
<div className={styles.panelHeader}>
|
||||
{step !== 'year' ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backBtn}
|
||||
onClick={() => setStep(step === 'day' ? 'month' : 'year')}
|
||||
aria-label="Back"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<span className={styles.backSpacer} />
|
||||
)}
|
||||
<div className={styles.steps}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.stepChip} ${step === 'year' ? styles.stepActive : ''}`}
|
||||
onClick={() => setStep('year')}
|
||||
>
|
||||
{draftYear}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.stepChip} ${step === 'month' ? styles.stepActive : ''}`}
|
||||
onClick={() => setStep('month')}
|
||||
>
|
||||
{MONTHS[draftMonth - 1]}
|
||||
</button>
|
||||
<span
|
||||
className={`${styles.stepChip} ${step === 'day' ? styles.stepActive : ''} ${styles.stepStatic}`}
|
||||
>
|
||||
Day
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{step === 'year' ? (
|
||||
<div className={styles.yearList}>
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year}
|
||||
type="button"
|
||||
className={`${styles.optionBtn} ${
|
||||
year === draftYear ? styles.optionSelected : ''
|
||||
} ${year === today.year ? styles.optionToday : ''}`}
|
||||
onClick={() => pickYear(year)}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 'month' ? (
|
||||
<div className={styles.monthGrid}>
|
||||
{MONTHS.map((label, index) => {
|
||||
const month = index + 1
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
type="button"
|
||||
className={`${styles.optionBtn} ${
|
||||
month === draftMonth ? styles.optionSelected : ''
|
||||
} ${
|
||||
draftYear === today.year && month === today.month
|
||||
? styles.optionToday
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => pickMonth(month)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 'day' ? (
|
||||
<div className={styles.dayPanel}>
|
||||
<div className={styles.weekdayRow}>
|
||||
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((d) => (
|
||||
<span key={d} className={styles.weekday}>
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.dayGrid}>
|
||||
{Array.from({ length: padStart }, (_, i) => (
|
||||
<span key={`pad-${i}`} className={styles.dayEmpty} />
|
||||
))}
|
||||
{Array.from({ length: dayCount }, (_, i) => {
|
||||
const day = i + 1
|
||||
const isSelected =
|
||||
selected?.year === draftYear &&
|
||||
selected?.month === draftMonth &&
|
||||
selected?.day === day
|
||||
const isToday =
|
||||
draftYear === today.year &&
|
||||
draftMonth === today.month &&
|
||||
day === today.day
|
||||
return (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
className={`${styles.dayBtn} ${
|
||||
isSelected ? styles.daySelected : ''
|
||||
} ${isToday && !isSelected ? styles.dayToday : ''} ${
|
||||
highlightDay === day && !isSelected ? styles.dayPrefill : ''
|
||||
}`}
|
||||
onClick={() => pickDay(day)}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>,
|
||||
document.body,
|
||||
)
|
||||
: null
|
||||
|
||||
return (
|
||||
<div className={styles.root} ref={rootRef}>
|
||||
<button
|
||||
@@ -171,129 +340,7 @@ export function StepDatePicker({
|
||||
<span className={styles.triggerText}>{display || placeholder}</span>
|
||||
<CalendarDays size={16} className={styles.icon} aria-hidden="true" />
|
||||
</button>
|
||||
|
||||
{open ? (
|
||||
<div className={styles.panel} role="dialog" aria-label="Choose date">
|
||||
<div className={styles.panelHeader}>
|
||||
{step !== 'year' ? (
|
||||
<button
|
||||
type="button"
|
||||
className={styles.backBtn}
|
||||
onClick={() => setStep(step === 'day' ? 'month' : 'year')}
|
||||
aria-label="Back"
|
||||
>
|
||||
<ChevronLeft size={16} />
|
||||
</button>
|
||||
) : (
|
||||
<span className={styles.backSpacer} />
|
||||
)}
|
||||
<div className={styles.steps}>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.stepChip} ${step === 'year' ? styles.stepActive : ''}`}
|
||||
onClick={() => setStep('year')}
|
||||
>
|
||||
{draftYear}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className={`${styles.stepChip} ${step === 'month' ? styles.stepActive : ''}`}
|
||||
onClick={() => setStep('month')}
|
||||
>
|
||||
{MONTHS[draftMonth - 1]}
|
||||
</button>
|
||||
<span
|
||||
className={`${styles.stepChip} ${step === 'day' ? styles.stepActive : ''} ${styles.stepStatic}`}
|
||||
>
|
||||
Day
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{step === 'year' ? (
|
||||
<div className={styles.yearList}>
|
||||
{years.map((year) => (
|
||||
<button
|
||||
key={year}
|
||||
type="button"
|
||||
className={`${styles.optionBtn} ${
|
||||
year === draftYear ? styles.optionSelected : ''
|
||||
} ${year === today.year ? styles.optionToday : ''}`}
|
||||
onClick={() => pickYear(year)}
|
||||
>
|
||||
{year}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 'month' ? (
|
||||
<div className={styles.monthGrid}>
|
||||
{MONTHS.map((label, index) => {
|
||||
const month = index + 1
|
||||
return (
|
||||
<button
|
||||
key={label}
|
||||
type="button"
|
||||
className={`${styles.optionBtn} ${
|
||||
month === draftMonth ? styles.optionSelected : ''
|
||||
} ${
|
||||
draftYear === today.year && month === today.month
|
||||
? styles.optionToday
|
||||
: ''
|
||||
}`}
|
||||
onClick={() => pickMonth(month)}
|
||||
>
|
||||
{label}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{step === 'day' ? (
|
||||
<div className={styles.dayPanel}>
|
||||
<div className={styles.weekdayRow}>
|
||||
{['Su', 'Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa'].map((d) => (
|
||||
<span key={d} className={styles.weekday}>
|
||||
{d}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
<div className={styles.dayGrid}>
|
||||
{Array.from({ length: padStart }, (_, i) => (
|
||||
<span key={`pad-${i}`} className={styles.dayEmpty} />
|
||||
))}
|
||||
{Array.from({ length: dayCount }, (_, i) => {
|
||||
const day = i + 1
|
||||
const isSelected =
|
||||
selected?.year === draftYear &&
|
||||
selected?.month === draftMonth &&
|
||||
selected?.day === day
|
||||
const isToday =
|
||||
draftYear === today.year &&
|
||||
draftMonth === today.month &&
|
||||
day === today.day
|
||||
return (
|
||||
<button
|
||||
key={day}
|
||||
type="button"
|
||||
className={`${styles.dayBtn} ${
|
||||
isSelected ? styles.daySelected : ''
|
||||
} ${isToday && !isSelected ? styles.dayToday : ''} ${
|
||||
highlightDay === day && !isSelected ? styles.dayPrefill : ''
|
||||
}`}
|
||||
onClick={() => pickDay(day)}
|
||||
>
|
||||
{day}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
{panel}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user