feature/clinic-lab-invitation #14
@@ -1,7 +1,7 @@
|
|||||||
// src/app/(dashboard)/billing/page.tsx
|
// src/app/(dashboard)/billing/page.tsx
|
||||||
'use client';
|
'use client';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { Search, Filter, Plus } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { Button } from '@/components/ui/common/Button';
|
import { Button } from '@/components/ui/common/Button';
|
||||||
import { Input } from '@/components/ui/common/Input';
|
import { Input } from '@/components/ui/common/Input';
|
||||||
import { Badge } from '@/components/ui/common/Badge';
|
import { Badge } from '@/components/ui/common/Badge';
|
||||||
@@ -44,11 +44,9 @@ export default function BillingPage() {
|
|||||||
<h1 className="text-2xl font-semibold text-text-primary">Billing</h1>
|
<h1 className="text-2xl font-semibold text-text-primary">Billing</h1>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="flex items-center gap-2"
|
|
||||||
disabled={!canEditBilling}
|
disabled={!canEditBilling}
|
||||||
title={!canEditBilling ? 'Read-only access for this organization.' : undefined}
|
title={!canEditBilling ? 'Read-only access for this organization.' : undefined}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 icon-flat" />
|
|
||||||
New Invoice
|
New Invoice
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -129,7 +127,7 @@ export default function BillingPage() {
|
|||||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||||
Paid
|
Paid
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
<th className="px-6 py-3 text-center text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||||
Status
|
Status
|
||||||
</th>
|
</th>
|
||||||
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
<th className="px-6 py-3 text-left text-xs font-medium text-text-muted uppercase tracking-wider">
|
||||||
@@ -158,7 +156,7 @@ export default function BillingPage() {
|
|||||||
<td className="px-6 py-4 text-sm text-text-primary">
|
<td className="px-6 py-4 text-sm text-text-primary">
|
||||||
${invoice.paid}
|
${invoice.paid}
|
||||||
</td>
|
</td>
|
||||||
<td className="px-6 py-4">
|
<td className="px-6 py-4 text-center align-middle">
|
||||||
<Badge
|
<Badge
|
||||||
variant={statusColors[invoice.status as keyof typeof statusColors]}
|
variant={statusColors[invoice.status as keyof typeof statusColors]}
|
||||||
className="capitalize"
|
className="capitalize"
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Check, Copy, Search, Send } from 'lucide-react';
|
|
||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
import {
|
import {
|
||||||
organizationApi,
|
organizationApi,
|
||||||
@@ -9,6 +8,7 @@ import {
|
|||||||
type CounterpartSearchResultDto,
|
type CounterpartSearchResultDto,
|
||||||
} from '@/lib/api/organization';
|
} from '@/lib/api/organization';
|
||||||
import { Button } from '@/components/ui/common/Button';
|
import { Button } from '@/components/ui/common/Button';
|
||||||
|
import { Badge, organizationLinkStatusVariant } from '@/components/ui/common/Badge';
|
||||||
import { Input } from '@/components/ui/common/Input';
|
import { Input } from '@/components/ui/common/Input';
|
||||||
import type { ApiError } from '@/types/api';
|
import type { ApiError } from '@/types/api';
|
||||||
|
|
||||||
@@ -39,6 +39,13 @@ function writeStoredInviteLinks(orgId: string, links: Record<string, StoredInvit
|
|||||||
window.localStorage.setItem(inviteLinksStorageKey(orgId), JSON.stringify(links));
|
window.localStorage.setItem(inviteLinksStorageKey(orgId), JSON.stringify(links));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** First letter uppercase, rest lowercase (e.g. ACTIVE → Active). */
|
||||||
|
function formatOrganizationStatusLabel(status: string): string {
|
||||||
|
if (!status) return status;
|
||||||
|
const lower = status.toLowerCase();
|
||||||
|
return lower.charAt(0).toUpperCase() + lower.slice(1);
|
||||||
|
}
|
||||||
|
|
||||||
function formatApiMessage(err: unknown): string {
|
function formatApiMessage(err: unknown): string {
|
||||||
if (!err || typeof err !== 'object') return 'Something went wrong';
|
if (!err || typeof err !== 'object') return 'Something went wrong';
|
||||||
const m = (err as ApiError).message;
|
const m = (err as ApiError).message;
|
||||||
@@ -202,7 +209,6 @@ export default function OrganizationsPage() {
|
|||||||
placeholder={`Search by ${counterpartLabel.toLowerCase()} name, email, or phone`}
|
placeholder={`Search by ${counterpartLabel.toLowerCase()} name, email, or phone`}
|
||||||
/>
|
/>
|
||||||
<Button type="button" isLoading={searchLoading} onClick={() => void runSearch()}>
|
<Button type="button" isLoading={searchLoading} onClick={() => void runSearch()}>
|
||||||
<Search className="w-4 h-4 mr-2" />
|
|
||||||
Search
|
Search
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -238,7 +244,6 @@ export default function OrganizationsPage() {
|
|||||||
disabled={!selectedResult}
|
disabled={!selectedResult}
|
||||||
onClick={() => void createLink()}
|
onClick={() => void createLink()}
|
||||||
>
|
>
|
||||||
<Send className="w-4 h-4 mr-2" />
|
|
||||||
Request link
|
Request link
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -303,9 +308,12 @@ export default function OrganizationsPage() {
|
|||||||
<td className="p-3 text-text-primary">{item.organizationName}</td>
|
<td className="p-3 text-text-primary">{item.organizationName}</td>
|
||||||
<td className="p-3 text-text-secondary">{item.ownerEmail}</td>
|
<td className="p-3 text-text-secondary">{item.ownerEmail}</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
<span className="inline-flex items-center rounded-full border border-border px-2 py-0.5 text-xs text-text-primary">
|
<Badge
|
||||||
{item.status}
|
variant={organizationLinkStatusVariant(item.status)}
|
||||||
</span>
|
fixedWidth
|
||||||
|
>
|
||||||
|
{formatOrganizationStatusLabel(item.status)}
|
||||||
|
</Badge>
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3">
|
<td className="p-3">
|
||||||
{item.kind === 'INVITATION' &&
|
{item.kind === 'INVITATION' &&
|
||||||
@@ -327,14 +335,7 @@ export default function OrganizationsPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{copiedId === item.id ? (
|
{copiedId === item.id ? 'Copied' : 'Copy link'}
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1">
|
|
||||||
{copiedId === item.id ? 'Copied' : 'Copy link'}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect, useMemo, useState } from 'react';
|
import { useEffect, useMemo, useState } from 'react';
|
||||||
import { Plus } from 'lucide-react';
|
|
||||||
import { Button } from '@/components/ui/common/Button';
|
import { Button } from '@/components/ui/common/Button';
|
||||||
import { patientsApi } from '@/lib/api/patients';
|
import { patientsApi } from '@/lib/api/patients';
|
||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
@@ -160,7 +159,6 @@ export default function PatientsPage() {
|
|||||||
<h1 className="text-2xl font-semibold text-text-primary">Patients</h1>
|
<h1 className="text-2xl font-semibold text-text-primary">Patients</h1>
|
||||||
<Button
|
<Button
|
||||||
variant="primary"
|
variant="primary"
|
||||||
className="flex items-center gap-2"
|
|
||||||
disabled={!canEditPatients}
|
disabled={!canEditPatients}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!canEditPatients) return;
|
if (!canEditPatients) return;
|
||||||
@@ -168,7 +166,6 @@ export default function PatientsPage() {
|
|||||||
}}
|
}}
|
||||||
title={!canEditPatients ? 'Read-only access for this organization.' : undefined}
|
title={!canEditPatients ? 'Read-only access for this organization.' : undefined}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 icon-flat" />
|
|
||||||
New Patient
|
New Patient
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import Link from 'next/link';
|
|||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
import { authApi } from '@/lib/api/auth';
|
import { authApi } from '@/lib/api/auth';
|
||||||
|
import { Button } from '@/components/ui/common/Button';
|
||||||
import type { SubscriptionAlertData } from '@/types/subscription';
|
import type { SubscriptionAlertData } from '@/types/subscription';
|
||||||
|
|
||||||
const PLAN_OPTIONS = [
|
const PLAN_OPTIONS = [
|
||||||
@@ -176,9 +177,9 @@ export default function SubscriptionsSettingsPage() {
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
className="inline-flex items-center justify-center rounded-[var(--radius-md)] bg-primary px-4 py-2 text-sm font-medium text-white hover:opacity-90 disabled:opacity-60"
|
variant="primary"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const selectedPlanLabel = selectedPlan?.name ?? 'the selected plan';
|
const selectedPlanLabel = selectedPlan?.name ?? 'the selected plan';
|
||||||
setPurchaseNotice(
|
setPurchaseNotice(
|
||||||
@@ -187,7 +188,7 @@ export default function SubscriptionsSettingsPage() {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Start purchase process
|
Start purchase process
|
||||||
</button>
|
</Button>
|
||||||
{purchaseNotice && (
|
{purchaseNotice && (
|
||||||
<div className="rounded-[var(--radius-md)] border border-emerald-500/30 bg-emerald-500/10 px-4 py-3">
|
<div className="rounded-[var(--radius-md)] border border-emerald-500/30 bg-emerald-500/10 px-4 py-3">
|
||||||
<p className="text-sm text-emerald-200">{purchaseNotice}</p>
|
<p className="text-sm text-emerald-200">{purchaseNotice}</p>
|
||||||
|
|||||||
@@ -16,10 +16,11 @@ import {
|
|||||||
formatAccessSummary,
|
formatAccessSummary,
|
||||||
type FeaturePermState,
|
type FeaturePermState,
|
||||||
} from './staff-permission-form';
|
} from './staff-permission-form';
|
||||||
import { UserPlus, Pencil, Trash2, Copy, Check, X, Clock3 } from 'lucide-react';
|
import { Pencil, Trash2, Copy, Check, X } from 'lucide-react';
|
||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
import { staffApi, type StaffMemberDto } from '@/lib/api/staff';
|
import { staffApi, type StaffMemberDto } from '@/lib/api/staff';
|
||||||
import { Button } from '@/components/ui/common/Button';
|
import { Button } from '@/components/ui/common/Button';
|
||||||
|
import { Badge } from '@/components/ui/common/Badge';
|
||||||
import { Input } from '@/components/ui/common/Input';
|
import { Input } from '@/components/ui/common/Input';
|
||||||
import { Checkbox } from '@/components/ui/common/Checkbox';
|
import { Checkbox } from '@/components/ui/common/Checkbox';
|
||||||
import type { ApiError } from '@/types/api';
|
import type { ApiError } from '@/types/api';
|
||||||
@@ -335,7 +336,6 @@ export default function StaffPage() {
|
|||||||
className="shrink-0"
|
className="shrink-0"
|
||||||
title={!canEdit ? 'Read-only access for this organization.' : undefined}
|
title={!canEdit ? 'Read-only access for this organization.' : undefined}
|
||||||
>
|
>
|
||||||
<UserPlus className="w-4 h-4 mr-2" />
|
|
||||||
Invite member
|
Invite member
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -410,14 +410,7 @@ export default function StaffPage() {
|
|||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{copiedInviteMembershipId === lastInviteInfo.membershipId ? (
|
{copiedInviteMembershipId === lastInviteInfo.membershipId ? 'Copied' : 'Copy link'}
|
||||||
<Check className="w-4 h-4" />
|
|
||||||
) : (
|
|
||||||
<Copy className="w-4 h-4" />
|
|
||||||
)}
|
|
||||||
<span className="ml-1">
|
|
||||||
{copiedInviteMembershipId === lastInviteInfo.membershipId ? 'Copied' : 'Copy link'}
|
|
||||||
</span>
|
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p className="text-xs text-text-muted">
|
<p className="text-xs text-text-muted">
|
||||||
@@ -438,7 +431,7 @@ export default function StaffPage() {
|
|||||||
<th className="p-3 font-medium">Name</th>
|
<th className="p-3 font-medium">Name</th>
|
||||||
<th className="p-3 font-medium">Email</th>
|
<th className="p-3 font-medium">Email</th>
|
||||||
<th className="p-3 font-medium">Role</th>
|
<th className="p-3 font-medium">Role</th>
|
||||||
<th className="p-3 font-medium">Status</th>
|
<th className="p-3 font-medium text-center">Status</th>
|
||||||
<th className="p-3 font-medium">Access</th>
|
<th className="p-3 font-medium">Access</th>
|
||||||
<th className="p-3 font-medium w-28">Actions</th>
|
<th className="p-3 font-medium w-28">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -455,20 +448,13 @@ export default function StaffPage() {
|
|||||||
<span className="text-text-secondary">Staff</span>
|
<span className="text-text-secondary">Staff</span>
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 align-middle">
|
<td className="p-3 align-middle text-center">
|
||||||
{m.isOwner || m.invitationStatus === 'ACTIVE' ? (
|
{m.isOwner || m.invitationStatus === 'ACTIVE' ? (
|
||||||
<span className="inline-flex items-center rounded-full border border-emerald-600/40 bg-emerald-600/15 px-2 py-0.5 text-xs text-emerald-400">
|
<Badge variant="success">Active</Badge>
|
||||||
Active
|
|
||||||
</span>
|
|
||||||
) : m.invitationStatus === 'PENDING' ? (
|
) : m.invitationStatus === 'PENDING' ? (
|
||||||
<span className="inline-flex items-center gap-1 rounded-full border border-amber-500/40 bg-amber-500/15 px-2 py-0.5 text-xs text-amber-300">
|
<Badge variant="warning">Pending</Badge>
|
||||||
<Clock3 className="h-3 w-3" />
|
|
||||||
Pending
|
|
||||||
</span>
|
|
||||||
) : (
|
) : (
|
||||||
<span className="inline-flex items-center rounded-full border border-red-500/40 bg-red-500/15 px-2 py-0.5 text-xs text-red-300">
|
<Badge variant="danger">Expired</Badge>
|
||||||
Expired
|
|
||||||
</span>
|
|
||||||
)}
|
)}
|
||||||
</td>
|
</td>
|
||||||
<td className="p-3 text-text-secondary max-w-md">
|
<td className="p-3 text-text-secondary max-w-md">
|
||||||
|
|||||||
@@ -1,31 +1,59 @@
|
|||||||
//src/components/ui/Badge.tsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
type BadgeVariant = 'success' | 'warning' | 'danger' | 'default';
|
export type BadgeVariant = 'success' | 'warning' | 'danger' | 'default';
|
||||||
|
|
||||||
interface BadgeProps {
|
interface BadgeProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
variant?: BadgeVariant;
|
variant?: BadgeVariant;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
/**
|
||||||
|
* Same pixel width for every badge (table columns).
|
||||||
|
* Set false only when the pill should shrink to the label.
|
||||||
|
*/
|
||||||
|
fixedWidth?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const variantStyles: Record<BadgeVariant, string> = {
|
const variantStyles: Record<BadgeVariant, string> = {
|
||||||
success: 'bg-emerald-900/30 text-emerald-300 border-emerald-700/60',
|
success: 'bg-emerald-900/30 text-emerald-300 border-emerald-700/60',
|
||||||
warning: 'bg-amber-900/30 text-amber-300 border-amber-700/60',
|
warning: 'bg-amber-900/30 text-amber-300 border-amber-700/60',
|
||||||
danger: 'bg-red-950/30 text-red-300 border-red-700/60',
|
danger: 'bg-red-950/30 text-red-300 border-red-700/60',
|
||||||
default: 'bg-background-secondary text-text-secondary border-border',
|
default: 'bg-background-secondary text-text-secondary border-border',
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** Explicit width + height so every row matches; flex centers label optically. */
|
||||||
|
const FIXED_LAYOUT_CLASS =
|
||||||
|
'w-[8rem] min-w-[8rem] max-w-[8rem] shrink-0 h-7 px-2 py-0';
|
||||||
|
|
||||||
export function Badge({
|
export function Badge({
|
||||||
children,
|
children,
|
||||||
variant = 'default',
|
variant = 'default',
|
||||||
className,
|
className,
|
||||||
|
fixedWidth = true,
|
||||||
}: BadgeProps) {
|
}: BadgeProps) {
|
||||||
return (
|
const layoutClass = fixedWidth
|
||||||
<span
|
? `${FIXED_LAYOUT_CLASS} justify-center text-center`
|
||||||
className={`px-2 py-1 text-xs font-medium rounded-lg border inline-block ${variantStyles[variant]} ${className || ''}`}
|
: 'min-h-[1.75rem] px-2.5 py-1 justify-center';
|
||||||
>
|
|
||||||
{children}
|
return (
|
||||||
</span>
|
<span
|
||||||
);
|
className={`inline-flex items-center box-border rounded-md border text-xs font-medium leading-none whitespace-nowrap ${variantStyles[variant]} ${layoutClass} ${className ?? ''}`}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Map organization link / invitation row status to badge variant. */
|
||||||
|
export function organizationLinkStatusVariant(status: string): BadgeVariant {
|
||||||
|
switch (status) {
|
||||||
|
case 'ACTIVE':
|
||||||
|
return 'success';
|
||||||
|
case 'PENDING':
|
||||||
|
return 'warning';
|
||||||
|
case 'REJECTED':
|
||||||
|
case 'EXPIRED':
|
||||||
|
return 'danger';
|
||||||
|
default:
|
||||||
|
return 'default';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
// src/components/ui/Button.tsx
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Loader2 } from 'lucide-react';
|
|
||||||
|
|
||||||
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost';
|
type ButtonVariant = 'primary' | 'secondary' | 'outline' | 'danger' | 'ghost';
|
||||||
type ButtonSize = 'sm' | 'md' | 'lg';
|
type ButtonSize = 'sm' | 'md' | 'lg';
|
||||||
@@ -25,23 +23,22 @@ export const Button: React.FC<ButtonProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const baseClasses =
|
const baseClasses =
|
||||||
'inline-flex items-center justify-center rounded-[var(--radius-md)] font-medium transition-all duration-200 ' +
|
'inline-flex items-center justify-center rounded-[var(--radius-md)] font-medium transition-all duration-200 ' +
|
||||||
'focus:outline-none focus:ring-2 focus:ring-primary/40 disabled:opacity-50 disabled:cursor-not-allowed';
|
'focus:outline-none focus:ring-2 focus:ring-primary/40 disabled:cursor-not-allowed';
|
||||||
|
|
||||||
const variantClasses: Record<ButtonVariant, string> = {
|
const variantClasses: Record<ButtonVariant, string> = {
|
||||||
primary:
|
primary:
|
||||||
'bg-primary text-primary-contrast hover:brightness-105 shadow-[0_0_0_1px_var(--color-primary-soft)]',
|
'bg-primary text-white hover:opacity-90 disabled:opacity-60',
|
||||||
|
|
||||||
secondary:
|
secondary:
|
||||||
'bg-surface-elevated text-text-primary border border-border hover:border-border-strong',
|
'bg-surface-elevated text-text-primary border border-border hover:border-border-strong disabled:opacity-50',
|
||||||
|
|
||||||
outline:
|
outline:
|
||||||
'border border-border text-text-primary hover:bg-background-card/70',
|
'border border-border text-text-primary hover:bg-background-card/70 disabled:opacity-50',
|
||||||
|
|
||||||
danger:
|
danger: 'bg-red-600 text-white hover:bg-red-700 disabled:opacity-50',
|
||||||
'bg-red-600 text-white hover:bg-red-700',
|
|
||||||
|
|
||||||
ghost:
|
ghost:
|
||||||
'text-text-secondary hover:text-text-primary hover:bg-background-card/70',
|
'text-text-secondary hover:text-text-primary hover:bg-background-card/70 disabled:opacity-50',
|
||||||
};
|
};
|
||||||
|
|
||||||
const sizeClasses: Record<ButtonSize, string> = {
|
const sizeClasses: Record<ButtonSize, string> = {
|
||||||
@@ -51,16 +48,15 @@ export const Button: React.FC<ButtonProps> = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const widthClass = fullWidth ? 'w-full' : '';
|
const widthClass = fullWidth ? 'w-full' : '';
|
||||||
|
const loadingClass = isLoading ? 'opacity-70 animate-pulse pointer-events-none' : '';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${widthClass} ${className}`}
|
className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${widthClass} ${loadingClass} ${className}`}
|
||||||
disabled={disabled || isLoading}
|
disabled={disabled || isLoading}
|
||||||
|
aria-busy={isLoading || undefined}
|
||||||
{...props}
|
{...props}
|
||||||
>
|
>
|
||||||
{isLoading && (
|
|
||||||
<Loader2 className="w-4 h-4 mr-2 animate-spin" />
|
|
||||||
)}
|
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useAuth } from '@/lib/hooks/useAuth';
|
import { useAuth } from '@/lib/hooks/useAuth';
|
||||||
import { Building2, Beaker, Mail, Plus } from 'lucide-react';
|
import { Building2, Beaker, Mail } from 'lucide-react';
|
||||||
import { Input } from '@/components/ui/common/Input';
|
import { Input } from '@/components/ui/common/Input';
|
||||||
import { Button } from '@/components/ui/common/Button';
|
import { Button } from '@/components/ui/common/Button';
|
||||||
|
|
||||||
@@ -55,7 +55,6 @@ export function OrganizationSelectorContent() {
|
|||||||
setIsCreateOpen((prev) => !prev);
|
setIsCreateOpen((prev) => !prev);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Plus className="h-4 w-4 mr-2 icon-flat" />
|
|
||||||
{isCreateOpen ? 'Cancel' : 'Create Organization'}
|
{isCreateOpen ? 'Cancel' : 'Create Organization'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user