fix(members): surface invite-link UI on the members page
The first-run "Invite or add members" step links to /members, but invite creation only lived in Customize, so the page dead-ended. Reuse the existing InvitesPanel: the members server page loads invites + space slug for admin/board and composes the panel into MembersClient via an inviteSlot prop (MembersClient stays unaware of invites). First-run copy updated to match.
This commit is contained in:
@@ -110,7 +110,7 @@ export default async function DashboardPage() {
|
|||||||
|
|
||||||
if (isFirstRun) {
|
if (isFirstRun) {
|
||||||
const steps = [
|
const steps = [
|
||||||
{ href: '/members', Ico: IcoUsers, title: 'Invite or add members', desc: 'Add people directly, or share an invite code from Customize.' },
|
{ href: '/members', Ico: IcoUsers, title: 'Invite or add members', desc: 'Add people directly, or generate a shareable join link right there.' },
|
||||||
{ href: '/customize', Ico: IcoPlus, title: 'Set up onboarding', desc: 'Decide what new members see and agree to when they join.' },
|
{ href: '/customize', Ico: IcoPlus, title: 'Set up onboarding', desc: 'Decide what new members see and agree to when they join.' },
|
||||||
{ href: '/tasks', Ico: IcoTask, title: 'Create your first task or chore', desc: 'Capture the recurring upkeep your space depends on.' },
|
{ href: '/tasks', Ico: IcoTask, title: 'Create your first task or chore', desc: 'Capture the recurring upkeep your space depends on.' },
|
||||||
{ href: '/settings', Ico: IcoPay, title: 'Connect payments', desc: 'Link a payment platform to track dues and reconcile income.' },
|
{ href: '/settings', Ico: IcoPay, title: 'Connect payments', desc: 'Link a payment platform to track dues and reconcile income.' },
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState } from 'react'
|
import { useState, type ReactNode } from 'react'
|
||||||
import { Plus, ChevronDown, Users } from 'lucide-react'
|
import { Plus, ChevronDown, Users } from 'lucide-react'
|
||||||
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
import { Empty, EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription } from '@/components/ui/empty'
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||||
@@ -18,6 +18,10 @@ interface Props {
|
|||||||
members: Member[]
|
members: Member[]
|
||||||
currentRole: string
|
currentRole: string
|
||||||
areaLeadRoles?: AreaLeadRole[]
|
areaLeadRoles?: AreaLeadRole[]
|
||||||
|
// Composed by the server page for admin/board: the existing InvitesPanel,
|
||||||
|
// so the members page is a real place to create and share a join link
|
||||||
|
// (the first-run "Invite or add members" step lands here).
|
||||||
|
inviteSlot?: ReactNode
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIER_COLORS: Record<string, string> = {
|
const TIER_COLORS: Record<string, string> = {
|
||||||
@@ -29,7 +33,7 @@ const TIER_COLORS: Record<string, string> = {
|
|||||||
|
|
||||||
const isAdmin = (role: string) => role === 'admin' || role === 'board'
|
const isAdmin = (role: string) => role === 'admin' || role === 'board'
|
||||||
|
|
||||||
export function MembersClient({ members: initialMembers, currentRole, areaLeadRoles = [] }: Props) {
|
export function MembersClient({ members: initialMembers, currentRole, areaLeadRoles = [], inviteSlot }: Props) {
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const confirm = useConfirm()
|
const confirm = useConfirm()
|
||||||
const [members, setMembers] = useState<Member[]>(initialMembers)
|
const [members, setMembers] = useState<Member[]>(initialMembers)
|
||||||
@@ -230,6 +234,7 @@ export function MembersClient({ members: initialMembers, currentRole, areaLeadRo
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="p-4 md:p-6">
|
<div className="p-4 md:p-6">
|
||||||
|
{inviteSlot && <div className="mb-6">{inviteSlot}</div>}
|
||||||
<div className="flex gap-3 mb-4">
|
<div className="flex gap-3 mb-4">
|
||||||
<div className="flex-1 relative">
|
<div className="flex-1 relative">
|
||||||
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
<svg className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { createClient } from '@/lib/supabase/server'
|
import { createClient } from '@/lib/supabase/server'
|
||||||
import { PageHeader, PageTitle } from '@/components/ui/page-title'
|
import { PageHeader, PageTitle } from '@/components/ui/page-title'
|
||||||
import { MembersClient } from './members-client'
|
import { MembersClient } from './members-client'
|
||||||
|
import { InvitesPanel } from '../customize/panels/invites-panel'
|
||||||
|
|
||||||
// member_directory_visibility values come from space settings:
|
// member_directory_visibility values come from space settings:
|
||||||
// admin_only — only admins see the directory
|
// admin_only — only admins see the directory
|
||||||
@@ -15,7 +16,7 @@ export default async function MembersPage() {
|
|||||||
|
|
||||||
const { data: self } = await supabase
|
const { data: self } = await supabase
|
||||||
.from('space_members')
|
.from('space_members')
|
||||||
.select('space_id, role')
|
.select('space_id, role, spaces(slug)')
|
||||||
.eq('user_id', user.id)
|
.eq('user_id', user.id)
|
||||||
.in('status', ['current', 'unverified', 'late'])
|
.in('status', ['current', 'unverified', 'late'])
|
||||||
.single()
|
.single()
|
||||||
@@ -73,11 +74,34 @@ export default async function MembersPage() {
|
|||||||
.order('area_name')
|
.order('area_name')
|
||||||
: { data: [] }
|
: { data: [] }
|
||||||
|
|
||||||
|
// Admin/board can create and share join links straight from this page (the
|
||||||
|
// same InvitesPanel used in Customize). This is where the first-run
|
||||||
|
// "Invite or add members" step sends people, so the UI has to be here.
|
||||||
|
const canInvite = self.role === 'admin' || self.role === 'board'
|
||||||
|
const spaceSlug = (self.spaces as { slug?: string } | null)?.slug ?? ''
|
||||||
|
const { data: invites } = canInvite
|
||||||
|
? await supabase
|
||||||
|
.from('space_invites')
|
||||||
|
.select('id, code, label, expires_at, max_uses, uses_count, is_enabled, role, created_at')
|
||||||
|
.eq('space_id', self.space_id)
|
||||||
|
.order('created_at', { ascending: false })
|
||||||
|
: { data: [] }
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MembersClient
|
<MembersClient
|
||||||
members={members ?? []}
|
members={members ?? []}
|
||||||
currentRole={self.role}
|
currentRole={self.role}
|
||||||
areaLeadRoles={(areaLeadRoles ?? []) as Array<{ id: string; area_name: string; lead_id: string | null }>}
|
areaLeadRoles={(areaLeadRoles ?? []) as Array<{ id: string; area_name: string; lead_id: string | null }>}
|
||||||
|
inviteSlot={
|
||||||
|
canInvite ? (
|
||||||
|
<InvitesPanel
|
||||||
|
isAdmin={isAdmin}
|
||||||
|
creatorRole={self.role}
|
||||||
|
spaceSlug={spaceSlug}
|
||||||
|
invites={(invites ?? []) as Array<{ id: string; code: string; label: string | null; expires_at: string | null; max_uses: number | null; uses_count: number; is_enabled: boolean; role: string; created_at: string }>}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user