Files
auditly/components/UiKit.tsx
Ra cf565df13e feat: major UI overhaul with new components and enhanced UX
- Add comprehensive Company Wiki feature with complete state management
  - CompanyWikiManager, empty states, invite modals
- Implement new Chat system with enhanced layout and components
  - ChatLayout, ChatSidebar, MessageThread, FileUploadInput
- Create modern Login and OTP verification flows
  - LoginNew page, OTPVerification component
- Add new Employee Forms system with enhanced controller
- Introduce Figma-based design components and multiple choice inputs
- Add new font assets (NeueMontreal) and robot images for onboarding
- Enhance existing components with improved styling and functionality
- Update build configuration and dependencies
- Remove deprecated ModernLogin component
2025-08-20 04:06:49 -07:00

366 lines
19 KiB
TypeScript

import React, { useState, ReactNode } from 'react';
import { NavLink, Outlet } from 'react-router-dom';
import { useTheme } from '../contexts/ThemeContext';
import { Theme, NavItem } from '../types';
import { useOrg } from '../contexts/OrgContext';
import { useAuth } from '../contexts/AuthContext';
import FigmaSidebar from './figma/Sidebar';
// ========== ICONS ==========
export const CompanyWikiIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M4 22h16a1 1 0 0 0 1-1V3a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v18a1 1 0 0 0 1 1zM8 7h8v2H8V7zm0 4h8v2H8v-2zm0 4h5v2H8v-2z" fill="currentColor" /></svg>
);
export const SubmissionsIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8l-6-6zM9 13v-2h6v2H9zm6 4H9v-2h6v2zm-6-8V4h1l5 5h-1V9H9z" fill="currentColor" /></svg>
);
export const ReportsIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 6h3a1 1 0 0 1 1 1v11a1 1 0 0 1-1 1H5a1 1 0 0 1-1-1V7a1 1 0 0 1 1-1h3V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2zm-4-2v2h-2V4h2zm-2 9v-2h-2v2h2zm4 0v-2h-2v2h2zm-4 4v-2h-2v2h2zm4 0v-2h-2v2h2zm-6-4H8V9h2v2z" fill="currentColor" /></svg>
);
export const ChatIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M21 11.5a8.5 8.5 0 0 1-17 0 8.5 8.5 0 0 1 17 0z" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /><path d="M21 11.5a8.5 8.5 0 0 0-8.5-8.5V3m0 17v-1.5a8.5 8.5 0 0 0 8.5-8.5H11" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" /></svg>
);
export const HelpIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-13a1 1 0 0 0-1 1v4a1 1 0 0 0 2 0v-4a1 1 0 0 0-1-1zm0 8a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z" fill="currentColor" /></svg>
);
export const SettingsIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19.14 12.94a2.001 2.001 0 0 0-2.28 0l-.71.71a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71zM4.86 12.94a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71a2 2 0 0 0-2.83 0zM12 10a2 2 0 1 0 0 4 2 2 0 0 0 0-4zm7.78-1.94a2 2 0 0 0-2.83 0l-.71.71a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71zM4.86 5.22a2 2 0 0 0 0 2.83l.71.71a2 2 0 0 0 2.83 0l.71-.71a2 2 0 0 0 0-2.83l-.71-.71a2 2 0 0 0-2.83 0z" fill="currentColor" /></svg>
);
export const CopyIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M16 2H8a2 2 0 0 0-2 2v12h2V4h8v4h4v8h2V8l-4-4h-2z" fill="currentColor" /><path d="M4 8h12v12H4z" fill="currentColor" opacity="0.5" /></svg>
);
export const PlusIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 13h-6v6h-2v-6H5v-2h6V5h2v6h6v2z" fill="currentColor" /></svg>
);
export const ChevronDownIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 15.5l-6-6 1.4-1.4 4.6 4.6 4.6-4.6L18 9.5l-6 6z" fill="currentColor" /></svg>
);
export const UploadIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M9 16h6v-6h4l-8-8-8 8h4v6zm-4 2h14v2H5v-2z" fill="currentColor" /></svg>
);
export const CheckIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z" fill="currentColor" /></svg>
);
export const WarningIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M1 21h22L12 2 1 21zm12-3h-2v-2h2v2zm0-4h-2V7h2v7z" fill="currentColor" /></svg>
);
export const DownloadIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z" fill="currentColor" /></svg>
);
export const MinusIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"><path d="M19 13H5v-2h14v2z" fill="currentColor" /></svg>
);
export const SunIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 3v1m0 16v1m9-9h-1M4 12H3m15.364 6.364l-.707-.707M6.343 6.343l-.707-.707m12.728 0l-.707.707M6.343 17.657l-.707.707M16 12a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
);
export const MoonIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M20.354 15.354A9 9 0 018.646 3.646 9.003 9.003 0 0012 21a9.003 9.003 0 008.354-5.646z" /></svg>
);
export const SystemIcon = ({ className }: { className?: string }) => (
<svg className={className} fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>
);
export const SendIcon = ({ className }: { className?: string }) => (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" xmlns="http://www.w3.org/2000/svg"><path d="M2.01 21L23 12 2.01 3 2 10l15 2-15 2z" /></svg>
);
const NAV_ITEMS: NavItem[] = [
{ href: '/company-wiki', label: 'Company Wiki', icon: CompanyWikiIcon },
{ href: '/submissions', label: 'Submissions', icon: SubmissionsIcon },
{ href: '/reports', label: 'Reports', icon: ReportsIcon },
{ href: '/chat', label: 'Chat', icon: ChatIcon },
{ href: '/help', label: 'Help', icon: HelpIcon },
];
const BOTTOM_NAV_ITEMS: NavItem[] = [
{ href: '/settings', label: 'Settings', icon: SettingsIcon }
];
// ========== LAYOUT COMPONENTS ==========
const Sidebar = () => {
const { org, issueInviteViaApi } = useOrg();
const { signOutUser } = useAuth();
const [showInviteModal, setShowInviteModal] = useState(false);
const [inviteForm, setInviteForm] = useState({ name: '', email: '', role: '', department: '' });
const [inviteLink, setInviteLink] = useState('');
const [emailLink, setEmailLink] = useState('');
const commonLinkClasses = "flex items-center w-full px-3 py-2.5 rounded-lg text-sm font-medium transition-colors duration-200";
const activeClass = "bg-[--sidebar-active-bg] text-[--sidebar-active-text]";
const inactiveClass = "text-[--sidebar-text] hover:bg-[--sidebar-active-bg] hover:text-[--sidebar-active-text]";
const handleInvite = async () => {
try {
const result = await issueInviteViaApi({
name: inviteForm.name,
email: inviteForm.email,
role: inviteForm.role,
department: inviteForm.department
});
setInviteLink(result.inviteLink);
setEmailLink(result.emailLink);
setInviteForm({ name: '', email: '', role: '', department: '' });
} catch (error) {
console.error('Failed to invite employee:', error);
}
};
const copyToClipboard = (text: string) => {
navigator.clipboard.writeText(text);
};
const renderNavItems = (items: NavItem[]) => items.map(item => (
<li key={item.href}>
<NavLink
to={item.href}
className={({ isActive }) => `${commonLinkClasses} ${isActive ? activeClass : inactiveClass}`}
>
<item.icon className="w-5 h-5 mr-3" />
<span>{item.label}</span>
</NavLink>
</li>
));
return (
<aside className="w-64 flex-shrink-0 bg-[--sidebar-bg] border-r border-[--border-color] flex flex-col p-4">
<div className="flex items-center mb-8">
<div className="w-8 h-8 bg-[--accent] rounded-full flex items-center justify-center font-bold text-white text-lg mr-2">A</div>
<h1 className="text-xl font-bold text-[--text-primary]">{org?.name || 'Auditly'}</h1>
</div>
<nav className="flex-1">
<ul className="space-y-2">
{renderNavItems(NAV_ITEMS)}
</ul>
</nav>
<div className="mt-auto space-y-4">
<ul className="space-y-2 border-t border-[--border-color] pt-4">
{renderNavItems(BOTTOM_NAV_ITEMS)}
</ul>
<div className="p-4 rounded-lg bg-[--background-tertiary] text-center">
<h3 className="font-bold text-sm text-[--text-primary]">Build [Company]'s Report</h3>
<p className="text-xs text-[--text-secondary] mt-1 mb-3">Share this form with your team members to capture valuable info about your company to train Auditly.</p>
<div className="flex space-x-2">
<Button size="sm" className="w-full" onClick={() => setShowInviteModal(true)}>
<PlusIcon className="w-4 h-4 mr-1" /> Invite
</Button>
<Button size="sm" variant="secondary" className="w-full" onClick={() => (emailLink || inviteLink) && copyToClipboard(emailLink || inviteLink)}>
<CopyIcon className="w-4 h-4 mr-1" /> Copy
</Button>
</div>
<Button size="sm" variant="ghost" className="w-full mt-2" onClick={signOutUser}>Sign out</Button>
</div>
{/* Invite Modal */}
{showInviteModal && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-[--background-secondary] p-6 rounded-lg max-w-md w-full mx-4">
<h3 className="text-lg font-semibold text-[--text-primary] mb-4">Invite Employee</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Name</label>
<input
type="text"
value={inviteForm.name}
onChange={(e) => setInviteForm(prev => ({ ...prev, name: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="Employee name"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Email</label>
<input
type="email"
value={inviteForm.email}
onChange={(e) => setInviteForm(prev => ({ ...prev, email: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="employee@company.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Role</label>
<input
type="text"
value={inviteForm.role}
onChange={(e) => setInviteForm(prev => ({ ...prev, role: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Software Engineer"
/>
</div>
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">Department</label>
<input
type="text"
value={inviteForm.department}
onChange={(e) => setInviteForm(prev => ({ ...prev, department: e.target.value }))}
className="w-full px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none focus:ring-2 focus:ring-blue-500"
placeholder="e.g. Engineering"
/>
</div>
{(inviteLink || emailLink) && (
<div className="space-y-3">
{emailLink && (
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
Email Link (for GET requests)
</label>
<div className="flex space-x-2">
<input
type="text"
value={emailLink}
readOnly
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
/>
<Button size="sm" onClick={() => copyToClipboard(emailLink)}>
<CopyIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
{inviteLink && (
<div>
<label className="block text-sm font-medium text-[--text-primary] mb-2">
App Link (direct)
</label>
<div className="flex space-x-2">
<input
type="text"
value={inviteLink}
readOnly
className="flex-1 px-3 py-2 border border-[--border-color] rounded-lg bg-[--background-primary] text-[--text-primary] focus:outline-none text-sm"
/>
<Button size="sm" onClick={() => copyToClipboard(inviteLink)}>
<CopyIcon className="w-4 h-4" />
</Button>
</div>
</div>
)}
</div>
)}
</div>
<div className="flex space-x-2 mt-6">
<Button
variant="secondary"
className="flex-1"
onClick={() => {
setShowInviteModal(false);
setInviteLink('');
setInviteForm({ name: '', email: '', role: '', department: '' });
}}
>
Cancel
</Button>
<Button
className="flex-1"
onClick={handleInvite}
disabled={!inviteForm.name || !inviteForm.email}
>
Generate Invite
</Button>
</div>
</div>
</div>
)}
</div>
</aside>
);
};
export const Layout = () => {
const { org } = useOrg();
return (
<div className="flex h-screen bg-Neutrals-NeutralSlate0">
<FigmaSidebar companyName={org?.name || "Auditly"} />
<main className="flex-1 overflow-y-auto">
<Outlet />
</main>
</div>
);
};
// ========== UI PRIMITIVES ==========
interface CardProps extends React.HTMLAttributes<HTMLDivElement> {
children: ReactNode;
className?: string;
padding?: 'sm' | 'md' | 'lg';
}
export const Card: React.FC<CardProps> = ({ children, className, padding = 'md', ...props }) => {
const paddingClasses = {
sm: 'p-4',
md: 'p-6',
lg: 'p-8',
};
return (
<div className={`bg-[--background-secondary] border border-[--border-color] rounded-xl shadow-sm ${paddingClasses[padding]} ${className || ''}`} {...props}>
{children}
</div>
);
};
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
children: ReactNode;
variant?: 'primary' | 'secondary' | 'danger' | 'ghost';
size?: 'sm' | 'md' | 'lg';
}
export const Button: React.FC<ButtonProps> = ({ children, variant = 'primary', size = 'md', className, ...props }) => {
const baseClasses = 'inline-flex items-center justify-center font-semibold rounded-lg focus:outline-none focus:ring-2 focus:ring-offset-2 transition-all duration-200 disabled:opacity-50 disabled:cursor-not-allowed';
const variantClasses = {
primary: 'bg-[--accent] text-[--accent-text] hover:bg-[--accent-hover] focus:ring-[--accent]',
secondary: 'bg-[--button-secondary-bg] text-[--text-primary] hover:bg-[--button-secondary-hover] focus:ring-[--accent] border border-[--border-color]',
danger: 'bg-[--status-red] text-white hover:bg-red-700 focus:ring-red-500',
ghost: 'bg-transparent text-[--text-primary] hover:bg-[--background-tertiary]'
};
const sizeClasses = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-6 py-3 text-base',
};
return (
<button className={`${baseClasses} ${variantClasses[variant]} ${sizeClasses[size]} ${className || ''}`} {...props}>
{children}
</button>
);
};
interface AccordionProps {
items: { question: string; answer: string }[];
}
export const Accordion = ({ items }: AccordionProps) => {
const [openIndex, setOpenIndex] = useState<number | null>(0);
const toggleItem = (index: number) => {
setOpenIndex(openIndex === index ? null : index);
};
return (
<div className="space-y-3">
{items.map((item, index) => (
<div key={index} className="border-b border-[--border-color]">
<button
onClick={() => toggleItem(index)}
className="w-full flex justify-between items-center py-4 text-left text-[--text-primary] font-medium"
>
<span>{item.question}</span>
{openIndex === index ? <MinusIcon className="w-5 h-5" /> : <PlusIcon className="w-5 h-5" />}
</button>
{openIndex === index && (
<div className="pb-4 text-[--text-secondary]">
{item.answer}
</div>
)}
</div>
))}
</div>
);
};